mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 21:17:58 +08:00
cb9d682a69
* 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>
279 lines
7.2 KiB
Go
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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|