Feature description
The caller's role is currently read only from the stored Membership record. orgs.ResolveCaller (backend/internal/orgs/orgs.go) sets rbac.Role(m.Role) from the database and never consults the token's group membership. The pieces for group-based roles are partly present but unused: auth.Validator.Validate (backend/internal/auth/validator.go) already extracts the groups claim into Claims.Groups using OIDC_GROUPS_CLAIM (default groups), and there is a dormant auth.IsAdmin(adminGroup, claims) helper plus an auth.Config.AdminGroup field. AdminGroup is not wired in backend/cmd/server/main.go, and ResolveCaller never calls IsAdmin, so group-driven roles are effectively unimplemented.
Proposal: derive a caller's Frames role from their Keycloak group membership, so role changes happen in Keycloak rather than through direct edits to membership rows.
- Mapping config: add a group to role map, for example
OIDC_ROLE_GROUP_MAP=frames-admins:admin,frames-publishers:publisher. This generalizes the existing single AdminGroup idea. Roles are viewer, publisher, admin from backend/internal/rbac/rbac.go.
- Multiple groups: when a user is in more than one mapped group, the highest role wins (
admin > publisher > viewer).
- Precedence with stored membership: decide whether an explicit membership row overrides the group mapping or the reverse. Recommendation: take the higher of the two so an explicit
admin grant is never silently downgraded by group data. This needs a decision.
- Org scoping: group strings are flat today, and the MVP is single-org via
SEED_ORG_SLUG. Decide whether a group maps to a role in that one org or whether the group name encodes an org (for example nebari:<org-slug>:admin) for later multi-org support.
- Keycloak side: the realm and the frames-web client must emit a
groups claim in the token via a group membership mapper. Decide full group path versus group name, and document the client mapper setup.
- Re-evaluation: the role is recomputed from the token on each request, so group changes take effect on the next token refresh. Note token lifetime when documenting expected propagation delay.
Code touch points:
backend/internal/auth/config.go: add the mapping config (extend or replace AdminGroup).
backend/internal/orgs/orgs.go: consume Claims.Groups when setting Caller.Role.
backend/cmd/server/main.go: wire the new env vars.
chart/values.yaml plus the deploy values in the GitOps repo: surface the mapping.
- Keep
rbac.Can as the single decision point. This issue only changes how Caller.Role is populated, not the permission checks.
Value and/or benefit
Access is managed in Keycloak, the identity provider already in use, instead of by hand-editing membership rows. Adding someone to a group gives them the right role, and removing them revokes it on the next token refresh. This reduces bespoke invite and membership bookkeeping and keeps authorization aligned with the organization's existing group structure.
Anything else?
Related: #22 (assign a default role to authenticated Keycloak users). That issue sets the baseline role for any valid login; this issue lets groups elevate it. Together they define how an authenticated identity becomes an authorized caller.
Feature description
The caller's role is currently read only from the stored
Membershiprecord.orgs.ResolveCaller(backend/internal/orgs/orgs.go) setsrbac.Role(m.Role)from the database and never consults the token's group membership. The pieces for group-based roles are partly present but unused:auth.Validator.Validate(backend/internal/auth/validator.go) already extracts thegroupsclaim intoClaims.GroupsusingOIDC_GROUPS_CLAIM(defaultgroups), and there is a dormantauth.IsAdmin(adminGroup, claims)helper plus anauth.Config.AdminGroupfield.AdminGroupis not wired inbackend/cmd/server/main.go, andResolveCallernever callsIsAdmin, so group-driven roles are effectively unimplemented.Proposal: derive a caller's Frames role from their Keycloak group membership, so role changes happen in Keycloak rather than through direct edits to membership rows.
OIDC_ROLE_GROUP_MAP=frames-admins:admin,frames-publishers:publisher. This generalizes the existing singleAdminGroupidea. Roles areviewer,publisher,adminfrombackend/internal/rbac/rbac.go.admin>publisher>viewer).admingrant is never silently downgraded by group data. This needs a decision.SEED_ORG_SLUG. Decide whether a group maps to a role in that one org or whether the group name encodes an org (for examplenebari:<org-slug>:admin) for later multi-org support.groupsclaim in the token via a group membership mapper. Decide full group path versus group name, and document the client mapper setup.Code touch points:
backend/internal/auth/config.go: add the mapping config (extend or replaceAdminGroup).backend/internal/orgs/orgs.go: consumeClaims.Groupswhen settingCaller.Role.backend/cmd/server/main.go: wire the new env vars.chart/values.yamlplus the deploy values in the GitOps repo: surface the mapping.rbac.Canas the single decision point. This issue only changes howCaller.Roleis populated, not the permission checks.Value and/or benefit
Access is managed in Keycloak, the identity provider already in use, instead of by hand-editing membership rows. Adding someone to a group gives them the right role, and removing them revokes it on the next token refresh. This reduces bespoke invite and membership bookkeeping and keeps authorization aligned with the organization's existing group structure.
Anything else?
Related: #22 (assign a default role to authenticated Keycloak users). That issue sets the baseline role for any valid login; this issue lets groups elevate it. Together they define how an authenticated identity becomes an authorized caller.