mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-07-18 04:54:54 +00:00
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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user