feat(oidc): add fine grained access control (#91)

* feat(access-control): fine grain tokens

* fix(docs): clean wording

* test(access-control): add tests for team extraction from access tokens and bucket info permissions

* test(vocabulary): add test for ExpandGlob function to reject non-glob patterns

* feat(helm): add multi-user access control documentation and schema support
This commit is contained in:
Noste
2026-07-11 17:57:28 +02:00
committed by GitHub
parent 3098e474f2
commit 0d804fbd39
49 changed files with 3224 additions and 234 deletions
+39
View File
@@ -225,6 +225,45 @@ config:
# ... additional OIDC settings
```
#### Multi-User Access Control (optional)
Scope what each OIDC user can see and do, based on the teams in their token
claims. **Absent by default**, so every authenticated user keeps full access.
When set, authorization becomes default-deny.
> **Not a security boundary.** This is UI-layer policy only. Anyone holding the
> Garage admin token or raw S3 keys bypasses it. See
> [docs/access-control.md](../../docs/access-control.md) for the full model and
> permission vocabulary.
```yaml
config:
auth:
oidc:
enabled: true
# OIDC claim (go-jmespath) listing the user's teams.
team_attribute_path: "groups"
# admin_role stays optional once access_control is set: unmatched users
# are denied rather than promoted to admin.
access_control:
presets:
bucket_readonly: [bucket.list, bucket.read, object.list, object.read]
bucket_owner: ["preset:bucket_readonly", bucket.create, bucket.update,
bucket.delete, object.write, object.delete]
teams:
- name: backend
claim_values: ["garage-team-backend"] # matched against the team_attribute_path claim
bindings:
- bucket_prefixes: ["backend-"]
permissions: ["preset:bucket_owner"]
- bucket_prefixes: ["shared-"]
permissions: ["preset:bucket_readonly"]
cluster_permissions: [cluster.status, cluster.health]
```
Admin-password and Garage-admin-token logins are always full admin in v1; only
OIDC users can be scoped to a team.
#### CORS Configuration
```yaml
+84
View File
@@ -322,11 +322,24 @@
"description": "Path to roles in the OIDC token claims",
"default": "resource_access.garage-ui.roles"
},
"team_attribute_path": {
"type": "string",
"description": "go-jmespath path to the team/group claim used by config.access_control (same convention as role_attribute_path). Required only when access_control.teams is set; empty disables team resolution",
"default": ""
},
"admin_role": {
"type": "string",
"description": "Role name that grants admin privileges",
"default": "admin"
},
"admin_roles": {
"type": "array",
"description": "Additional admin role names. A user is granted admin if ANY of their roles matches admin_role or any entry here",
"items": {
"type": "string"
},
"default": []
},
"tls_skip_verify": {
"type": "boolean",
"description": "Skip TLS certificate verification (only for testing)",
@@ -429,6 +442,77 @@
"default": "json"
}
}
},
"access_control": {
"type": "object",
"description": "Optional team-based access control (issue #33). When present (even empty) authorization is default-deny: OIDC users get only what their teams grant, and users matching no team are denied everywhere. When omitted, every authenticated user has full access. UI-layer policy only, NOT a security boundary: anyone holding the Garage admin token or S3 keys bypasses it. Requires auth.oidc.team_attribute_path when teams are set. See docs/access-control.md",
"properties": {
"presets": {
"type": "object",
"description": "Named, reusable permission lists referenced from bindings/cluster_permissions via the 'preset:<name>' syntax. Presets may reference other presets",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"teams": {
"type": "array",
"description": "Maps IdP claim values to bucket-prefix bindings and cluster-level permissions",
"items": {
"type": "object",
"required": ["name", "claim_values"],
"properties": {
"name": {
"type": "string",
"description": "Unique team name"
},
"claim_values": {
"type": "array",
"description": "Values matched (exact string, no wildcards) against the team_attribute_path claim",
"items": {
"type": "string"
},
"minItems": 1
},
"bindings": {
"type": "array",
"description": "Prefix-scoped permission grants over buckets whose names match one of the prefixes",
"items": {
"type": "object",
"required": ["bucket_prefixes", "permissions"],
"properties": {
"bucket_prefixes": {
"type": "array",
"description": "Bucket-name prefixes this binding applies to. Use '*' to match every bucket",
"items": {
"type": "string"
},
"minItems": 1
},
"permissions": {
"type": "array",
"description": "Prefix-scoped permissions, preset references (preset:<name>), or trailing-star globs (e.g. bucket.*, object.*)",
"items": {
"type": "string"
},
"minItems": 1
}
}
}
},
"cluster_permissions": {
"type": "array",
"description": "Global (cluster-level) permissions granted to the team, e.g. cluster.status, cluster.health",
"items": {
"type": "string"
}
}
}
}
}
}
}
}
},
+20
View File
@@ -113,6 +113,11 @@ config:
role_attribute_path: "resource_access.garage-ui.roles"
admin_role: "admin"
admin_roles: []
# Team-based access control (optional). OIDC claim (go-jmespath, same
# convention as role_attribute_path) listing the user's teams. Required
# only when config.access_control.teams is set; leave empty to disable.
# See config.access_control below and docs/access-control.md.
team_attribute_path: ""
# TLS settings
tls_skip_verify: false
# Session settings
@@ -147,6 +152,21 @@ config:
# Options: json, text
format: "json"
# access_control:
# presets:
# bucket_readonly: [bucket.list, bucket.read, object.list, object.read]
# bucket_owner: ["preset:bucket_readonly", bucket.create, bucket.update,
# bucket.delete, object.write, object.delete]
# teams:
# - name: backend
# claim_values: ["garage-team-backend"] # matched against the team_attribute_path claim
# bindings:
# - bucket_prefixes: ["backend-"]
# permissions: ["preset:bucket_owner"]
# - bucket_prefixes: ["shared-"]
# permissions: ["preset:bucket_readonly"]
# cluster_permissions: [cluster.status, cluster.health]
# Pod annotations
podAnnotations: {}