Files
moebiusbandandClaude Opus 4.8 3474690adc feat(auth): map OIDC token roles to CC roles via configurable mapping
OIDC roles were translated by a hardcoded switch that only recognised the
literal names user/admin/manager/support (dropping api) and required the
IdP to emit exactly those names.

Add an optional auth.oidc.role-mapping (OIDC role/group claim value -> CC
role) so operators can map their own realm/client role names, including
api. The mapping is the sole source of roles: only mapped names are
honored, unmapped token roles are ignored, and users without any mapped
role receive the base "user" role. Mapping targets are validated once at
startup. Role assignment stays authoritative on every login as before.

Note: deployments relying on the IdP emitting literal CC role names must
now map them explicitly (e.g. "admin": "admin").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 66451678484b
2026-06-19 11:28:20 +02:00

83 lines
2.1 KiB
Go

// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
// All rights reserved. This file is part of cc-backend.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package auth
import (
"reflect"
"testing"
"github.com/ClusterCockpit/cc-lib/v2/schema"
)
func TestMapOIDCRoles(t *testing.T) {
var (
user = schema.GetRoleString(schema.RoleUser)
admin = schema.GetRoleString(schema.RoleAdmin)
support = schema.GetRoleString(schema.RoleSupport)
api = schema.GetRoleString(schema.RoleAPI)
)
mapping := map[string]string{
"cc-admins": admin,
"cc-support": support,
"cc-api": api,
"staff": support, // second name mapping to the same role
}
tests := []struct {
name string
oidcRoles []string
mapping map[string]string
want []string
}{
{
name: "explicit mapping to elevated roles",
oidcRoles: []string{"cc-admins", "cc-api"},
mapping: mapping,
want: []string{admin, api},
},
{
name: "unmapped names are ignored (no identity fallback)",
oidcRoles: []string{"admin", "support", "unknown"},
mapping: mapping,
want: []string{user},
},
{
name: "mix of mapped and unmapped keeps only mapped",
oidcRoles: []string{"cc-admins", "admin", "noise"},
mapping: mapping,
want: []string{admin},
},
{
name: "empty token roles default to user",
oidcRoles: nil,
mapping: mapping,
want: []string{user},
},
{
name: "no mapping configured defaults to user",
oidcRoles: []string{"cc-admins", "admin"},
mapping: map[string]string{},
want: []string{user},
},
{
name: "duplicate target roles are deduplicated and sorted",
oidcRoles: []string{"cc-support", "staff", "cc-admins"},
mapping: mapping,
want: []string{admin, support},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := mapOIDCRoles(tt.oidcRoles, tt.mapping)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("mapOIDCRoles() = %v, want %v", got, tt.want)
}
})
}
}