Feat admin rbac migration (#1)
Some checks failed
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-frontend (push) Successful in 5s
CI/CD / build-and-deploy (push) Has been cancelled
CI/CD / test-sqlite (push) Has been cancelled
CI/CD / test-go-pg (push) Has been cancelled

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #1.
This commit is contained in:
2026-03-26 17:30:36 +00:00
committed by xcaliber
parent b9180d4184
commit 5836ddad21
37 changed files with 381 additions and 547 deletions

View File

@@ -31,7 +31,6 @@ type OIDCConfig struct {
RedirectURL string // e.g. "https://switchboard.corp/api/v1/auth/oidc/callback"
AutoActivate bool // auto-activate new users (default true)
DefaultTeam string // team ID for auto-provisioned users (optional)
DefaultRole string // "user" (default) or "admin"
RolesClaim string // JWT claim for role mapping (default "realm_access.roles")
GroupsClaim string // JWT claim for group sync (default "groups")
AdminRole string // IdP role value that maps to admin (default "admin")
@@ -92,9 +91,6 @@ func NewOIDCProvider(cfg OIDCConfig) (*OIDCProvider, error) {
if cfg.ClientID == "" {
return nil, fmt.Errorf("OIDC_CLIENT_ID is required")
}
if cfg.DefaultRole == "" {
cfg.DefaultRole = models.UserRoleUser
}
if cfg.RolesClaim == "" {
cfg.RolesClaim = "realm_access.roles"
}
@@ -164,11 +160,9 @@ func (p *OIDCProvider) Authenticate(c *gin.Context, stores store.Stores) (*Resul
return nil, ErrInactive
}
// Re-evaluate role on every login
role := p.resolveRole(claims)
if role != user.Role {
stores.Users.Update(ctx, user.ID, map[string]interface{}{"role": role})
user.Role = role
// Sync Admins group based on IdP role claim
if p.isIdPAdmin(claims) {
AddToAdminsGroup(ctx, stores, user.ID)
}
// Sync groups
@@ -310,14 +304,14 @@ func (p *OIDCProvider) validateToken(tokenString string) (*oidcClaims, error) {
return claims, nil
}
func (p *OIDCProvider) resolveRole(claims *oidcClaims) string {
// Check realm_access.roles for admin role
// isIdPAdmin checks whether the OIDC claims include the configured admin role.
func (p *OIDCProvider) isIdPAdmin(claims *oidcClaims) bool {
for _, role := range claims.RealmAccess.Roles {
if role == p.cfg.AdminRole {
return models.UserRoleAdmin
return true
}
}
return p.cfg.DefaultRole
return false
}
// syncGroups compares IdP groups claim with internal group memberships
@@ -454,14 +448,12 @@ func (p *OIDCProvider) autoProvision(
displayName = username
}
role := p.resolveRole(claims)
handle := UniqueHandle(ctx, stores.Users, models.HandleFromName(username))
user := &models.User{
Username: strings.ToLower(username),
Email: strings.ToLower(email),
DisplayName: displayName,
Role: role,
IsActive: true,
AuthSource: string(ModeOIDC),
ExternalID: &sub,
@@ -474,6 +466,11 @@ func (p *OIDCProvider) autoProvision(
log.Printf("[auth/oidc] auto-provisioned user %s (sub=%s, email=%s)", user.ID, sub, email)
EnsureEveryoneGroup(ctx, stores, user.ID)
if p.isIdPAdmin(claims) {
AddToAdminsGroup(ctx, stores, user.ID)
}
// Auto-add to default team
if p.cfg.DefaultTeam != "" {
if err := stores.Teams.AddMember(ctx, p.cfg.DefaultTeam, user.ID, "member"); err != nil {