Skip to content

feat: OIDC group→role mapping, per-provider scopes and groups claim - #73

Merged
ValgulNecron merged 4 commits into
mainfrom
feat/oidc-group-role-mapping
Jul 5, 2026
Merged

ValgulNecron merged 4 commits into
mainfrom
feat/oidc-group-role-mapping

Conversation

@ValgulNecron

Copy link
Copy Markdown
Collaborator

Closes the main OIDC gap for beta.6: IdP groups can now drive Gameplane roles.

API

  • Provider config gains scopes (extra scopes beyond openid/profile/email, deduped), groupsClaim (default groups), roleMappings (admin/operator/viewer group lists), defaultRole (""|viewer|operator|admin|deny).
  • Callback extracts the groups claim (string array or single string); highest matching role wins; deny returns a neutral 403 (login-privacy rule respected — /auth/providers does not expose the new fields pre-auth).
  • When any mapping is set, roles re-sync on every login: users.role + the '*'-namespace binding (namespace-scoped bindings untouched). No mappings → existing behavior unchanged.
  • Last-admin lockout guard: a resync that would demote the sole remaining user-manager is skipped (login still succeeds) and logged — mirrors the manual user-management guard.
  • validateAuth rejects the new fields on kind=local and validates scope tokens, role enum, and group lists.
  • Legacy Helm-flag single provider intentionally unsupported for mapping (documented in docs/oidc.md).

Web

  • Authentication settings provider form: Scopes, Groups claim, Role mapping (admin/operator/viewer group lists), Default role select incl. "Deny sign-in" (disabled until a mapping exists, matching API validation). Empty inputs serialize to absent fields.

Tests

  • Fake-IdP flow tests: promote/demote sync, deny, no-mapping passthrough, absent vs empty groups, AuthCodeURL scopes, last-admin guard (sole admin keeps role; demotes normally when a second admin exists), validateAuth cases.
  • Web: form rendering, exact payload round-trip incl. deny, stored mappings surviving unrelated edits.

Design: Pencil node L0cAF (Authentication screen role-mapping section) on branch chore/design-beta6-screens.

🤖 Generated with Claude Code

OIDC providers gain four config fields: scopes (extra OAuth scopes past
the base openid/profile/email set, deduplicated), groupsClaim (the
ID-token claim carrying group memberships, default "groups"),
roleMappings (per-role IdP group lists; most privileged match wins,
admin > operator > viewer), and defaultRole (fallback when nothing
matches: viewer/operator/admin, or deny to refuse the login with 403).
When roleMappings is set the IdP becomes authoritative: every login
re-syncs users.role and the cluster-wide ('*') role binding in one
transaction, leaving namespace-scoped bindings alone. Providers without
mappings keep the existing behavior — new users start as viewer and
manual promotions survive re-login. validateAuth rejects the new fields
on the local provider, malformed scope tokens, blank claims, unknown
default roles, defaultRole without mappings, and empty mapping groups.
Covered by extractGroups/computeRole table tests, fake-IdP flow tests
(first-login mapping, demotion resync, manual-promotion persistence,
deny, AuthCodeURL scopes), and validateAuth cases.

Signed-off-by: valgulnecron <39313199+ValgulNecron@users.noreply.github.com>
The IdP-authoritative role resync could lock an install out of user
administration: if the sole admin's group mapping stopped granting a
manager role, their own login would demote them. syncUserRole now
mirrors the manual users handler's guard (RoleGrantsUserManagement /
UserManagesUsers / UserManagerCount, count <= 1) and skips the role
write — login still succeeds, stored role kept — with a slog warning.
Covered by fake-IdP tests for both the skip (sole admin keeps admin)
and the normal demotion when a second user-manager exists. Also notes
in docs/oidc.md that the legacy Helm-flag provider has no group->role
mapping support.

Signed-off-by: valgulnecron <39313199+ValgulNecron@users.noreply.github.com>
The add-provider form in Admin Settings -> Authentication now exposes
the group->role mapping fields the API accepts: extra OAuth scopes
(space- or comma-separated), the groups claim (placeholder "groups"),
a Role mapping subsection with comma-separated Admin/Operator/Viewer
group lists (highest matching role wins; roles re-sync on every login
while any mapping is set), and a Default role select whose Deny sign-in
option refuses unmatched logins. Empty inputs serialize to absent
fields (never [""]), group names are trimmed, and the Default role
select stays locked until a mapping exists since the API rejects
defaultRole without roleMappings. AuthProvider in lib/config.ts gains
the optional scopes/groupsClaim/roleMappings/defaultRole fields, and
the section tests cover field rendering, the exact saved JSON payload
including deny, and a stored provider's mapping fields surviving an
unrelated save.

Signed-off-by: valgulnecron <39313199+ValgulNecron@users.noreply.github.com>
TestHandleCallback_RoleMappingResync demoted an admin who was the test
DB's only user-manager, so the new lockout guard (correctly) skipped
the demotion and the operator/operator expectations went stale. Seed a
second admin before the logins — mirroring the guard-inverse test — so
the demotion path applies again; assertions unchanged. The other resync
tests are unaffected: every remaining resolveOrLinkUser call site uses
syncRole=false, the deny test never reaches an existing user, and the
two guard tests cover skip/apply explicitly.

Signed-off-by: valgulnecron <39313199+ValgulNecron@users.noreply.github.com>
@ValgulNecron
ValgulNecron merged commit 6dce5a7 into main Jul 5, 2026
20 checks passed
@ValgulNecron
ValgulNecron deleted the feat/oidc-group-role-mapping branch July 5, 2026 20:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant