From 6c8e5e2d416b2e0aba1c7bb7016d4fc6e691b867 Mon Sep 17 00:00:00 2001 From: Jian Qiu Date: Mon, 16 Mar 2026 13:33:37 +0800 Subject: [PATCH] Add more unit tests for constrained impersonation (#136737) * Add more unit tests for constrained impersonation test cases for large number of groups/extra test cases for system:masters constrained impersonation is not allowed Signed-off-by: Jian Qiu * Validate each authz request in the constrained impersonation unit test Signed-off-by: Jian Qiu --------- Signed-off-by: Jian Qiu --- .../constrained_impersonation_test.go | 484 +++++++++++++++--- 1 file changed, 406 insertions(+), 78 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/impersonation/constrained_impersonation_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/impersonation/constrained_impersonation_test.go index 84ff2335f99..5069baa0b03 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/impersonation/constrained_impersonation_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/impersonation/constrained_impersonation_test.go @@ -50,7 +50,7 @@ type constrainedImpersonationTest struct { t *testing.T constrainedImpersonationHandler *constrainedImpersonationHandler - checkedAttrs []authorizer.Attributes + authzChecks []authzCheck echoCalled bool auditEvents []*auditinternal.Event @@ -62,6 +62,11 @@ type constrainedImpersonationTest struct { lock sync.Mutex } +type authzCheck struct { + attributes authorizer.AttributesRecord + decision authorizer.Decision +} + func (c *constrainedImpersonationTest) ProcessEvents(evs ...*auditinternal.Event) bool { for _, e := range evs { c.auditEvents = append(c.auditEvents, e.DeepCopy()) // event is mutated in place so capture its current state @@ -69,13 +74,18 @@ func (c *constrainedImpersonationTest) ProcessEvents(evs ...*auditinternal.Event return true } -func (c *constrainedImpersonationTest) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { - c.lock.Lock() - c.checkedAttrs = append(c.checkedAttrs, a) - c.lock.Unlock() - +func (c *constrainedImpersonationTest) Authorize(ctx context.Context, a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) { u := a.GetUser() + defer func() { + c.lock.Lock() + c.authzChecks = append(c.authzChecks, authzCheck{ + attributes: comparableAttributes(a), + decision: decision, + }) + c.lock.Unlock() + }() + if u.GetName() == "sa-impersonater" && a.GetVerb() == "impersonate:serviceaccount" && a.GetResource() == "serviceaccounts" { return authorizer.DecisionAllow, "", nil } @@ -114,6 +124,36 @@ func (c *constrainedImpersonationTest) Authorize(ctx context.Context, a authoriz return authorizer.DecisionAllow, "", nil } + // many-groups-impersonater: can impersonate users and any groups via wildcard + if u.GetName() == "many-groups-impersonater" && a.GetVerb() == "impersonate:user-info" && a.GetResource() == "users" { + return authorizer.DecisionAllow, "", nil + } + if u.GetName() == "many-groups-impersonater" && a.GetVerb() == "impersonate:user-info" && a.GetResource() == "groups" && a.GetName() == "*" { + return authorizer.DecisionAllow, "", nil + } + + // many-extras-impersonater: can impersonate users and any extras via wildcard + if u.GetName() == "many-extras-impersonater" && a.GetVerb() == "impersonate:user-info" && a.GetResource() == "users" { + return authorizer.DecisionAllow, "", nil + } + if u.GetName() == "many-extras-impersonater" && a.GetVerb() == "impersonate:user-info" && a.GetResource() == "userextras" && a.GetSubresource() == "*" && a.GetName() == "*" { + return authorizer.DecisionAllow, "", nil + } + + // mixed-impersonater: can impersonate users, specific groups, and specific extras (but NOT via wildcards) + if u.GetName() == "mixed-impersonater" && a.GetVerb() == "impersonate:user-info" && a.GetResource() == "users" { + return authorizer.DecisionAllow, "", nil + } + // Allow specific group names, but NOT wildcard + if u.GetName() == "mixed-impersonater" && a.GetVerb() == "impersonate:user-info" && a.GetResource() == "groups" && a.GetName() != "*" { + return authorizer.DecisionAllow, "", nil + } + // Allow specific extras for tags.io/env, but NOT wildcard + if u.GetName() == "mixed-impersonater" && a.GetVerb() == "impersonate:user-info" && a.GetResource() == "userextras" && + a.GetSubresource() == "tags.io/env" && a.GetName() != "*" { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionNoOpinion, "deny by default", nil } @@ -256,10 +296,10 @@ func (t *testRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { } func (c *constrainedImpersonationTest) assertAttributes(r testRequest) { - checkedAttrs := c.checkedAttrs - c.checkedAttrs = nil + authzChecks := c.authzChecks + c.authzChecks = nil - require.Equal(c.t, len(r.expectedAttributes), len(checkedAttrs)) + require.Len(c.t, authzChecks, len(r.expectedAuthzChecks)) // normally all authorization checks are done against the requestor // but in some cases such as associated-node, we check against a slightly different user @@ -268,9 +308,10 @@ func (c *constrainedImpersonationTest) assertAttributes(r testRequest) { expectedAttributesUser = r.requestor } - for i := range len(r.expectedAttributes) { - expectedAttributes := withUser(r.expectedAttributes[i], expectedAttributesUser) - require.Equal(c.t, expectedAttributes, comparableAttributes(checkedAttrs[i])) + for i := range len(r.expectedAuthzChecks) { + expectedAttributes := withUser(r.expectedAuthzChecks[i].attributes, expectedAttributesUser) + require.Equal(c.t, expectedAttributes, authzChecks[i].attributes, "authz check %d: attributes mismatch", i) + require.Equal(c.t, r.expectedAuthzChecks[i].decision, authzChecks[i].decision, "authz check %d: decision mismatch", i) } } @@ -471,6 +512,21 @@ func withUser(a authorizer.AttributesRecord, u *user.DefaultInfo) authorizer.Att return a } +func expectAuthzCheck(attributes authorizer.AttributesRecord, decision authorizer.Decision) authzCheck { + return authzCheck{ + attributes: attributes, + decision: decision, + } +} + +func expectAllow(attributes authorizer.AttributesRecord) authzCheck { + return expectAuthzCheck(attributes, authorizer.DecisionAllow) +} + +func expectDeny(attributes authorizer.AttributesRecord) authzCheck { + return expectAuthzCheck(attributes, authorizer.DecisionNoOpinion) +} + func outerCacheKey(wantedUser, requestor *user.DefaultInfo, req *request.RequestInfo) impersonationCacheKey { attributes := withUser(requestInfoToAttributes(req), requestor) return impersonationCacheKey{ @@ -502,7 +558,7 @@ type testRequest struct { expectedMessage string expectedAttributesUser *user.DefaultInfo // nil means use requestor - expectedAttributes []authorizer.AttributesRecord + expectedAuthzChecks []authzCheck expectedCache *expectedCache expectedCode int @@ -595,9 +651,9 @@ func associatedNodeTestCase() []testRequest { impersonatedUser: &user.DefaultInfo{Name: "system:node:node1"}, // node matches expectedImpersonatedUser: node1FullUserInfo, expectedAttributesUser: saDefaultOnAnyNode, - expectedAttributes: []authorizer.AttributesRecord{ - withImpersonateOnAttributes(getSecretRequest, "associated-node"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "nodes", Name: "*"}, "associated-node"), + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getSecretRequest, "associated-node")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "nodes", Name: "*"}, "associated-node")), }, expectedCache: cacheWithOnlyNode1Data, expectedCode: http.StatusOK, @@ -613,7 +669,7 @@ func associatedNodeTestCase() []testRequest { impersonatedUser: &user.DefaultInfo{Name: "system:node:node2"}, // node matches expectedImpersonatedUser: node2FullUserInfo, expectedAttributesUser: saDefaultOnAnyNode, - expectedAttributes: nil, // no authz checks for the second request + expectedAuthzChecks: nil, // no authz checks for the second request expectedCache: cacheWithOnlyNode1Data, expectedCode: http.StatusOK, expectedAttemptMode: "associated-node", @@ -626,10 +682,10 @@ func associatedNodeTestCase() []testRequest { impersonatedUser: &user.DefaultInfo{Name: "system:node:node1"}, // node does not match expectedMessage: `nodes.authentication.k8s.io "node1" is forbidden: User "system:serviceaccount:default:default" cannot impersonate:arbitrary-node resource "nodes" in API group "authentication.k8s.io" at the cluster scope: deny by default`, expectedAttributesUser: nil, - expectedAttributes: []authorizer.AttributesRecord{ - withImpersonateOnAttributes(getSecretRequest, "arbitrary-node"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "nodes", Name: "node1"}, "arbitrary-node"), - withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:node:node1"}), + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getSecretRequest, "arbitrary-node")), + expectDeny(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "nodes", Name: "node1"}, "arbitrary-node")), + expectDeny(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:node:node1"})), }, expectedCache: cacheWithOnlyNode1Data, expectedCode: http.StatusForbidden, @@ -647,8 +703,8 @@ func associatedNodeTestCase() []testRequest { impersonatedUser: &user.DefaultInfo{Name: "system:node:node2"}, // node matches expectedImpersonatedUser: node2FullUserInfo, expectedAttributesUser: saDefaultOnAnyNode, - expectedAttributes: []authorizer.AttributesRecord{ - withImpersonateOnAttributes(getPodRequest, "associated-node"), // one authz check because different request info + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "associated-node")), // one authz check because different request info }, expectedCache: cacheWithMultipleRequests, expectedCode: http.StatusOK, @@ -664,7 +720,7 @@ func associatedNodeTestCase() []testRequest { impersonatedUser: &user.DefaultInfo{Name: "system:node:node1"}, // node matches expectedImpersonatedUser: node1FullUserInfo, expectedAttributesUser: nil, - expectedAttributes: nil, // no authz checks for pod request via node1 + expectedAuthzChecks: nil, // no authz checks for pod request via node1 expectedCache: cacheWithMultipleRequests, expectedCode: http.StatusOK, expectedAttemptMode: "associated-node", @@ -739,10 +795,10 @@ func TestConstrainedImpersonationFilter(t *testing.T) { request: getPodRequest, requestor: &user.DefaultInfo{Name: "tester"}, impersonatedUser: anyone, - expectedAttributes: []authorizer.AttributesRecord{ - withImpersonateOnAttributes(getPodRequest, "user-info"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"}, "user-info"), - withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"}), + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + expectDeny(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"}, "user-info")), + expectDeny(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"})), }, expectedCache: nil, expectedCode: http.StatusForbidden, @@ -765,9 +821,9 @@ func TestConstrainedImpersonationFilter(t *testing.T) { requestor: userImpersonator, impersonatedUser: anyone, expectedImpersonatedUser: anyoneAuthenticated, - expectedAttributes: []authorizer.AttributesRecord{ - withImpersonateOnAttributes(getPodRequest, "user-info"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"}, "user-info"), + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"}, "user-info")), }, expectedCache: &expectedCache{ modeIdx: map[string]string{ @@ -796,8 +852,8 @@ func TestConstrainedImpersonationFilter(t *testing.T) { requestor: userImpersonator, impersonatedUser: anyone, expectedImpersonatedUser: anyoneAuthenticated, - expectedAttributes: []authorizer.AttributesRecord{ - withImpersonateOnAttributes(getAnotherPodRequest, "user-info"), + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getAnotherPodRequest, "user-info")), }, expectedCache: &expectedCache{ modeIdx: map[string]string{ @@ -826,9 +882,9 @@ func TestConstrainedImpersonationFilter(t *testing.T) { request: createPodRequest, requestor: userImpersonator, impersonatedUser: anyone, - expectedAttributes: []authorizer.AttributesRecord{ - withImpersonateOnAttributes(createPodRequest, "user-info"), - withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"}), + expectedAuthzChecks: []authzCheck{ + expectDeny(withImpersonateOnAttributes(createPodRequest, "user-info")), + expectDeny(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"})), }, expectedCache: &expectedCache{ modeIdx: map[string]string{ @@ -865,9 +921,9 @@ func TestConstrainedImpersonationFilter(t *testing.T) { requestor: saImpersonator, impersonatedUser: saUser, expectedImpersonatedUser: saUserAuthenticated, - expectedAttributes: []authorizer.AttributesRecord{ - withImpersonateOnAttributes(getPodRequest, "serviceaccount"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "serviceaccounts", Namespace: "default", Name: "default"}, "serviceaccount"), + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "serviceaccount")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "serviceaccounts", Namespace: "default", Name: "default"}, "serviceaccount")), }, expectedCache: &expectedCache{ modeIdx: map[string]string{ @@ -903,10 +959,10 @@ func TestConstrainedImpersonationFilter(t *testing.T) { request: getPodRequest, requestor: saImpersonator, impersonatedUser: nodeUser, - expectedAttributes: []authorizer.AttributesRecord{ - withImpersonateOnAttributes(getPodRequest, "arbitrary-node"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "nodes", Name: "node1"}, "arbitrary-node"), - withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:node:node1"}), + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "arbitrary-node")), + expectDeny(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "nodes", Name: "node1"}, "arbitrary-node")), + expectDeny(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:node:node1"})), }, expectedCache: nil, expectedCode: http.StatusForbidden, @@ -928,9 +984,9 @@ func TestConstrainedImpersonationFilter(t *testing.T) { request: createPodRequest, requestor: nodeImpersonator, impersonatedUser: nodeUser, - expectedAttributes: []authorizer.AttributesRecord{ - withImpersonateOnAttributes(createPodRequest, "arbitrary-node"), - withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:node:node1"}), + expectedAuthzChecks: []authzCheck{ + expectDeny(withImpersonateOnAttributes(createPodRequest, "arbitrary-node")), + expectDeny(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:node:node1"})), }, expectedCache: nil, expectedCode: http.StatusForbidden, @@ -952,9 +1008,9 @@ func TestConstrainedImpersonationFilter(t *testing.T) { requestor: nodeImpersonator, impersonatedUser: nodeUser, expectedImpersonatedUser: nodeUserAuthenticated, - expectedAttributes: []authorizer.AttributesRecord{ - withImpersonateOnAttributes(getPodRequest, "arbitrary-node"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "nodes", Name: "node1"}, "arbitrary-node"), + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "arbitrary-node")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "nodes", Name: "node1"}, "arbitrary-node")), }, expectedCache: &expectedCache{ modeIdx: map[string]string{ @@ -994,12 +1050,12 @@ func TestConstrainedImpersonationFilter(t *testing.T) { Groups: []string{"extra-setter-scopes"}, Extra: map[string][]string{"pandas.io/scopes": {"scope-a", "scope-b"}}, }, - expectedAttributes: []authorizer.AttributesRecord{ - withImpersonateOnAttributes(getPodRequest, "user-info"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:admin"}, "user-info"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "groups", Name: "extra-setter-scopes"}, "user-info"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "scope-a"}, "user-info"), - withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:admin"}), + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:admin"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "groups", Name: "extra-setter-scopes"}, "user-info")), + expectDeny(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "scope-a"}, "user-info")), + expectDeny(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:admin"})), }, expectedCache: nil, expectedCode: http.StatusForbidden, @@ -1032,17 +1088,17 @@ func TestConstrainedImpersonationFilter(t *testing.T) { Groups: []string{"system:authenticated"}, Extra: map[string][]string{"pandas.io/scopes": {"scope-a", "scope-b", "5", "4", "3", "2", "1"}}, }, - expectedAttributes: []authorizer.AttributesRecord{ - withImpersonateOnAttributes(getPodRequest, "user-info"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:admin"}, "user-info"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "*", Name: "*"}, "user-info"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "scope-a"}, "user-info"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "scope-b"}, "user-info"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "5"}, "user-info"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "4"}, "user-info"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "3"}, "user-info"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "2"}, "user-info"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "1"}, "user-info"), + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:admin"}, "user-info")), + expectDeny(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "*", Name: "*"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "scope-a"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "scope-b"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "5"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "4"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "3"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "2"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "1"}, "user-info")), }, expectedCache: &expectedCache{ modeIdx: map[string]string{ @@ -1112,10 +1168,10 @@ func TestConstrainedImpersonationFilter(t *testing.T) { }, }, impersonatedUser: &user.DefaultInfo{Name: "system:node:node1"}, - expectedAttributes: []authorizer.AttributesRecord{ - withImpersonateOnAttributes(getPodRequest, "arbitrary-node"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "nodes", Name: "node1"}, "arbitrary-node"), - withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:node:node1"}), + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "arbitrary-node")), + expectDeny(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "nodes", Name: "node1"}, "arbitrary-node")), + expectDeny(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:node:node1"})), }, expectedCache: nil, expectedCode: http.StatusForbidden, @@ -1141,10 +1197,10 @@ func TestConstrainedImpersonationFilter(t *testing.T) { Name: "system:admin", Groups: []string{"system:authenticated"}, }, - expectedAttributes: []authorizer.AttributesRecord{ - withImpersonateOnAttributes(getPodRequest, "user-info"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:admin"}, "user-info"), - withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:admin"}), + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + expectDeny(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:admin"}, "user-info")), + expectAllow(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:admin"})), }, expectedCache: &expectedCache{ modeIdx: map[string]string{ @@ -1163,6 +1219,277 @@ func TestConstrainedImpersonationFilter(t *testing.T) { }, }, }, + { + name: "system:masters-group-not-allowed", + requests: []testRequest{ + { + request: getPodRequest, + requestor: userImpersonator, + impersonatedUser: &user.DefaultInfo{ + Name: "admin-user", + Groups: []string{user.SystemPrivilegedGroup}, + }, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "admin-user"}, "user-info")), + expectDeny(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "admin-user"})), + }, + expectedCache: nil, + expectedCode: http.StatusForbidden, + expectedMessage: `groups.authentication.k8s.io "system:masters" is forbidden: User "user-impersonater" cannot impersonate:user-info resource "groups" in API group "authentication.k8s.io" at the cluster scope: impersonating the system:masters group is not allowed`, + expectedAttemptMode: "", + expectedAttemptDecision: "denied", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 2, // impersonate-on:user-info:get, impersonate:user-info users + "legacy/denied": 1, // impersonate users + }, + }, + }, + }, + { + name: "many-groups-wildcard-allowed", + requests: []testRequest{ + { + request: getPodRequest, + requestor: &user.DefaultInfo{ + Name: "many-groups-impersonater", + }, + impersonatedUser: &user.DefaultInfo{ + Name: "bob", + Groups: []string{"group1", "group2", "group3", "group4"}, + }, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "bob", + Groups: []string{"group1", "group2", "group3", "group4", "system:authenticated"}, + }, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "bob"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "groups", Name: "*"}, "user-info")), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + "many-groups-impersonater": "impersonate:user-info", + }, + modes: map[string]expectedModeCache{ + "impersonate:user-info": { + outer: map[impersonationCacheKey]*user.DefaultInfo{ + outerCacheKey(&user.DefaultInfo{ + Name: "bob", + Groups: []string{"group1", "group2", "group3", "group4"}, + }, &user.DefaultInfo{Name: "many-groups-impersonater"}, getPodRequest): { + Name: "bob", + Groups: []string{"group1", "group2", "group3", "group4", "system:authenticated"}, + }, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: &user.DefaultInfo{ + Name: "bob", + Groups: []string{"group1", "group2", "group3", "group4"}, + }, + requestor: &user.DefaultInfo{Name: "many-groups-impersonater"}, + }: { + Name: "bob", + Groups: []string{"group1", "group2", "group3", "group4", "system:authenticated"}, + }, + }, + }, + }, + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "user-info", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 3, // impersonate-on:user-info:get, impersonate:user-info users/groups + }, + }, + }, + }, + { + name: "many-extras-wildcard-allowed", + requests: []testRequest{ + { + request: getPodRequest, + requestor: &user.DefaultInfo{ + Name: "many-extras-impersonater", + }, + impersonatedUser: &user.DefaultInfo{ + Name: "alice", + Extra: map[string][]string{ + "scopes.example.com/key1": {"val1"}, + "scopes.example.com/key2": {"val2"}, + "scopes.example.com/key3": {"val3"}, + "scopes.example.com/key4": {"val4"}, + }, + }, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "alice", + Groups: []string{"system:authenticated"}, + Extra: map[string][]string{ + "scopes.example.com/key1": {"val1"}, + "scopes.example.com/key2": {"val2"}, + "scopes.example.com/key3": {"val3"}, + "scopes.example.com/key4": {"val4"}, + }, + }, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "alice"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "*", Name: "*"}, "user-info")), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + "many-extras-impersonater": "impersonate:user-info", + }, + modes: map[string]expectedModeCache{ + "impersonate:user-info": { + outer: map[impersonationCacheKey]*user.DefaultInfo{ + outerCacheKey(&user.DefaultInfo{ + Name: "alice", + Extra: map[string][]string{ + "scopes.example.com/key1": {"val1"}, + "scopes.example.com/key2": {"val2"}, + "scopes.example.com/key3": {"val3"}, + "scopes.example.com/key4": {"val4"}, + }, + }, &user.DefaultInfo{Name: "many-extras-impersonater"}, getPodRequest): { + Name: "alice", + Groups: []string{"system:authenticated"}, + Extra: map[string][]string{ + "scopes.example.com/key1": {"val1"}, + "scopes.example.com/key2": {"val2"}, + "scopes.example.com/key3": {"val3"}, + "scopes.example.com/key4": {"val4"}, + }, + }, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: &user.DefaultInfo{ + Name: "alice", + Extra: map[string][]string{ + "scopes.example.com/key1": {"val1"}, + "scopes.example.com/key2": {"val2"}, + "scopes.example.com/key3": {"val3"}, + "scopes.example.com/key4": {"val4"}, + }, + }, + requestor: &user.DefaultInfo{Name: "many-extras-impersonater"}, + }: { + Name: "alice", + Groups: []string{"system:authenticated"}, + Extra: map[string][]string{ + "scopes.example.com/key1": {"val1"}, + "scopes.example.com/key2": {"val2"}, + "scopes.example.com/key3": {"val3"}, + "scopes.example.com/key4": {"val4"}, + }, + }, + }, + }, + }, + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "user-info", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 3, // impersonate-on:user-info:get, impersonate:user-info users/extras + }, + }, + }, + }, + { + name: "mixed-groups-extras-wildcard-denied-individuals-allowed", + requests: []testRequest{ + { + request: getPodRequest, + requestor: &user.DefaultInfo{ + Name: "mixed-impersonater", + }, + impersonatedUser: &user.DefaultInfo{ + Name: "frank", + Groups: []string{"dev", "ops", "security", "audit"}, + Extra: map[string][]string{ + "tags.io/env": {"prod", "staging", "dev"}, + }, + }, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "frank", + Groups: []string{"dev", "ops", "security", "audit", "system:authenticated"}, + Extra: map[string][]string{ + "tags.io/env": {"prod", "staging", "dev"}, + }, + }, + expectedAuthzChecks: []authzCheck{ + // impersonate-on check passes + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + // user check passes + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "frank"}, "user-info")), + // wildcard groups check FAILS (mixed-impersonater doesn't allow wildcard groups) + expectDeny(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "groups", Name: "*"}, "user-info")), + // individual group checks PASS - demonstrating fallback after wildcard failure + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "groups", Name: "dev"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "groups", Name: "ops"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "groups", Name: "security"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "groups", Name: "audit"}, "user-info")), + // individual extra checks PASS (wildcard not attempted for extras with single subresource) + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "tags.io/env", Name: "prod"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "tags.io/env", Name: "staging"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "tags.io/env", Name: "dev"}, "user-info")), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + "mixed-impersonater": "impersonate:user-info", + }, + modes: map[string]expectedModeCache{ + "impersonate:user-info": { + outer: map[impersonationCacheKey]*user.DefaultInfo{ + outerCacheKey(&user.DefaultInfo{ + Name: "frank", + Groups: []string{"dev", "ops", "security", "audit"}, + Extra: map[string][]string{ + "tags.io/env": {"prod", "staging", "dev"}, + }, + }, &user.DefaultInfo{Name: "mixed-impersonater"}, getPodRequest): { + Name: "frank", + Groups: []string{"dev", "ops", "security", "audit", "system:authenticated"}, + Extra: map[string][]string{ + "tags.io/env": {"prod", "staging", "dev"}, + }, + }, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: &user.DefaultInfo{ + Name: "frank", + Groups: []string{"dev", "ops", "security", "audit"}, + Extra: map[string][]string{ + "tags.io/env": {"prod", "staging", "dev"}, + }, + }, + requestor: &user.DefaultInfo{Name: "mixed-impersonater"}, + }: { + Name: "frank", + Groups: []string{"dev", "ops", "security", "audit", "system:authenticated"}, + Extra: map[string][]string{ + "tags.io/env": {"prod", "staging", "dev"}, + }, + }, + }, + }, + }, + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "user-info", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 9, // impersonate-on:user-info:get; impersonate:user-info users; 4*impersonate:user-info groups; 3*impersonate:user-info extras + "user-info/denied": 1, // impersonate:user-info groups "*" + }, + }, + }, + }, { name: "continuous-same-allowed-user-requests", requests: []testRequest{ @@ -1174,9 +1501,9 @@ func TestConstrainedImpersonationFilter(t *testing.T) { Name: "bob", Groups: []string{"system:authenticated"}, }, - expectedAttributes: []authorizer.AttributesRecord{ - withImpersonateOnAttributes(getDeploymentRequest, "user-info"), - withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "bob"}, "user-info"), + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getDeploymentRequest, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "bob"}, "user-info")), }, expectedCache: &expectedCache{ modeIdx: map[string]string{ @@ -1214,7 +1541,8 @@ func TestConstrainedImpersonationFilter(t *testing.T) { Name: "bob", Groups: []string{"system:authenticated"}, }, - expectedCode: http.StatusOK, + expectedAuthzChecks: nil, // no authz checks for cached request + expectedCode: http.StatusOK, expectedCache: &expectedCache{ modeIdx: map[string]string{ userImpersonator.Name: "impersonate:user-info",