Files
zot/pkg/api/authn_internal_test.go
T
Akash Kumar cb9d682a69 feat(auth): map OpenID groups claim (#3999)
* feat(auth): map OpenID groups claim

Signed-off-by: Akash Kumar <meakash7902@gmail.com>

* fix(auth): refine OIDC claim mapping logs

Signed-off-by: Akash Kumar <meakash7902@gmail.com>

* refactor(auth): collapse OIDC username fallback into nested if

Reuse the empty-username branch for the email fallback so the value is
checked once and the failure path lives next to the recovery attempt.

Signed-off-by: Akash Kumar <meakash7902@gmail.com>

* refactor(auth): consolidate OIDC claim extraction into authn.go

Move getOpenIDClaimMapping, getOpenIDUsername, and appendOpenIDGroups
out of routes.go into authn.go alongside a new extractOpenIDIdentity
helper that owns the username/groups extraction flow. This keeps the
HTTP callback in routes.go thin and groups OIDC plumbing with the rest
of the authentication code.

Also:
- Filter nil and empty entries consistently across the []any, []string,
  and string branches of appendOpenIDGroups, with new test cases
  covering []any{nil, ""} and []string{"admin","",...}.
- Surface a Warn log when an operator-configured username claim is
  missing/empty so the fallback to email isn't silent.
- Rename openid_claim_mapping_internal_test.go to authn_internal_test.go
  and drop the build tags that aren't needed for the internal tests.

Signed-off-by: Akash Kumar <meakash7902@gmail.com>

---------

Signed-off-by: Akash Kumar <meakash7902@gmail.com>
2026-05-01 11:59:51 +03:00

279 lines
7.2 KiB
Go

package api
import (
"testing"
"github.com/zitadel/oidc/v3/pkg/oidc"
"zotregistry.dev/zot/v2/pkg/api/config"
)
func TestGetOpenIDClaimMapping(t *testing.T) {
t.Parallel()
tests := []struct {
name string
authConfig *config.AuthConfig
providerName string
expectedUsername string
expectedGroups string
expectedConfigured bool
}{
{
name: "nil auth config uses defaults",
expectedUsername: defaultUsernameClaim,
expectedGroups: defaultGroupsClaim,
expectedConfigured: false,
},
{
name: "empty provider uses defaults",
authConfig: &config.AuthConfig{
OpenID: &config.OpenIDConfig{
Providers: map[string]config.OpenIDProviderConfig{},
},
},
expectedUsername: defaultUsernameClaim,
expectedGroups: defaultGroupsClaim,
expectedConfigured: false,
},
{
name: "missing provider uses defaults",
authConfig: &config.AuthConfig{
OpenID: &config.OpenIDConfig{
Providers: map[string]config.OpenIDProviderConfig{},
},
},
providerName: "oidc",
expectedUsername: defaultUsernameClaim,
expectedGroups: defaultGroupsClaim,
expectedConfigured: false,
},
{
name: "provider without claim mapping uses defaults",
authConfig: &config.AuthConfig{
OpenID: &config.OpenIDConfig{
Providers: map[string]config.OpenIDProviderConfig{
"oidc": {},
},
},
},
providerName: "oidc",
expectedUsername: defaultUsernameClaim,
expectedGroups: defaultGroupsClaim,
expectedConfigured: false,
},
{
name: "custom username and groups claims",
authConfig: &config.AuthConfig{
OpenID: &config.OpenIDConfig{
Providers: map[string]config.OpenIDProviderConfig{
"oidc": {
ClaimMapping: &config.ClaimMapping{
Username: "preferred_username",
Groups: "roles",
},
},
},
},
},
providerName: "oidc",
expectedUsername: "preferred_username",
expectedGroups: "roles",
expectedConfigured: true,
},
{
name: "custom groups keeps default username",
authConfig: &config.AuthConfig{
OpenID: &config.OpenIDConfig{
Providers: map[string]config.OpenIDProviderConfig{
"oidc": {
ClaimMapping: &config.ClaimMapping{
Groups: "roles",
},
},
},
},
},
providerName: "oidc",
expectedUsername: defaultUsernameClaim,
expectedGroups: "roles",
expectedConfigured: false,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
usernameClaim, groupsClaim, usernameConfigured := getOpenIDClaimMapping(test.authConfig, test.providerName)
if usernameClaim != test.expectedUsername {
t.Fatalf("expected username claim %q, got %q", test.expectedUsername, usernameClaim)
}
if groupsClaim != test.expectedGroups {
t.Fatalf("expected groups claim %q, got %q", test.expectedGroups, groupsClaim)
}
if usernameConfigured != test.expectedConfigured {
t.Fatalf("expected usernameConfigured %t, got %t", test.expectedConfigured, usernameConfigured)
}
})
}
}
func TestGetOpenIDUsername(t *testing.T) {
t.Parallel()
info := &oidc.UserInfo{
Subject: "subject-id",
UserInfoProfile: oidc.UserInfoProfile{
Name: "Full Name",
PreferredUsername: "preferred-user",
},
UserInfoEmail: oidc.UserInfoEmail{Email: "user@example.com"},
Claims: map[string]any{
"custom_username": "custom-user",
"numeric": 42,
},
}
tests := []struct {
name string
info *oidc.UserInfo
claim string
expected string
}{
{name: "nil userinfo", claim: defaultUsernameClaim},
{name: "preferred username", info: info, claim: "preferred_username", expected: "preferred-user"},
{name: "email", info: info, claim: defaultUsernameClaim, expected: "user@example.com"},
{name: "subject", info: info, claim: "sub", expected: "subject-id"},
{name: "name", info: info, claim: "name", expected: "Full Name"},
{name: "custom string claim", info: info, claim: "custom_username", expected: "custom-user"},
{name: "custom non-string claim", info: info, claim: "numeric"},
{name: "missing custom claim", info: info, claim: "missing"},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
username := getOpenIDUsername(test.info, test.claim)
if username != test.expected {
t.Fatalf("expected username %q, got %q", test.expected, username)
}
})
}
}
func TestAppendOpenIDGroups(t *testing.T) {
t.Parallel()
tests := []struct {
name string
groups []string
claims map[string]any
claim string
expected []string
expectedFound bool
}{
{
name: "appends any slice",
groups: []string{"existing"},
claims: map[string]any{"roles": []any{"dev", 7}},
claim: "roles",
expected: []string{"existing", "dev", "7"},
expectedFound: true,
},
{
name: "skips nil and empty entries in any slice",
claims: map[string]any{"roles": []any{"dev", nil, ""}},
claim: "roles",
expected: []string{"dev"},
expectedFound: true,
},
{
name: "appends string slice",
claims: map[string]any{"roles": []string{"admin", "ops"}},
claim: "roles",
expected: []string{"admin", "ops"},
expectedFound: true,
},
{
name: "skips empty entries in string slice",
claims: map[string]any{"roles": []string{"admin", "", "ops"}},
claim: "roles",
expected: []string{"admin", "ops"},
expectedFound: true,
},
{
name: "appends non-empty string",
claims: map[string]any{"roles": "admin"},
claim: "roles",
expected: []string{"admin"},
expectedFound: true,
},
{
name: "finds empty string",
claims: map[string]any{"roles": ""},
claim: "roles",
expected: nil,
expectedFound: true,
},
{
name: "finds empty any slice",
claims: map[string]any{"roles": []any{}},
claim: "roles",
expected: nil,
expectedFound: true,
},
{
name: "finds empty string slice",
claims: map[string]any{"roles": []string{}},
claim: "roles",
expected: nil,
expectedFound: true,
},
{
name: "does not find missing claim",
claims: map[string]any{},
claim: "roles",
expected: nil,
expectedFound: false,
},
{
name: "does not find unsupported claim type",
claims: map[string]any{"roles": 7},
claim: "roles",
expected: nil,
expectedFound: false,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
groups, found := appendOpenIDGroups(test.groups, test.claims, test.claim)
if found != test.expectedFound {
t.Fatalf("expected found %t, got %t", test.expectedFound, found)
}
if len(groups) != len(test.expected) {
t.Fatalf("expected groups %v, got %v", test.expected, groups)
}
for i := range test.expected {
if groups[i] != test.expected[i] {
t.Fatalf("expected groups %v, got %v", test.expected, groups)
}
}
})
}
}