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 <jqiu@redhat.com>

* Validate each authz request in the constrained impersonation unit test

Signed-off-by: Jian Qiu <jqiu@redhat.com>

---------

Signed-off-by: Jian Qiu <jqiu@redhat.com>
This commit is contained in:
Jian Qiu
2026-03-16 13:33:37 +08:00
committed by GitHub
parent 8cfc85428a
commit 6c8e5e2d41

View File

@@ -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",