diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/impersonation/cache.go b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/impersonation/cache.go index cf73107896a..c0ef42c583a 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/impersonation/cache.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/impersonation/cache.go @@ -18,11 +18,12 @@ package impersonation import ( "crypto/sha256" + "encoding/binary" + "encoding/hex" "fmt" "hash/fnv" "time" - - "golang.org/x/crypto/cryptobyte" + "unsafe" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" @@ -60,21 +61,19 @@ func modeIndexCacheKey(attributes authorizer.Attributes) string { key := attributes.GetUser().GetName() // hash the name so our cache size is predicable regardless of the size of usernames // collisions do not matter for this logic as it simply changes the ordering of the modes used - hash := fnvSum128a([]byte(key)) - return fmt.Sprintf("%x", hash) -} - -func fnvSum128a(data []byte) []byte { h := fnv.New128a() - h.Write(data) + h.Write([]byte(key)) var sum [16]byte - return h.Sum(sum[:0]) + h.Sum(sum[:0]) + buf := make([]byte, 32) // 16 bytes of hash -> 32 hex chars + hex.Encode(buf, sum[:]) + return toString(buf) // only safe because buf is heap allocated via make } func newModeIndexCache() *modeIndexCache { return &modeIndexCache{ - // each entry is roughly ~24 bytes (16 bytes for the hashed key, 8 bytes for value) - // thus at even 10k entries, we should use less than 1 MB memory + // each entry is roughly ~100 bytes (hashed string key + int value + LRU list/map overhead) + // thus at even 10k entries, we should use about 1 MB memory // this hardcoded size allows us to remember many users without leaking memory cache: lru.New(10_000), } @@ -246,7 +245,7 @@ func buildKey(wantedUser *user.DefaultInfo, attributes authorizer.Attributes) (s } }) - return b.build() + return b.build(), nil } func addUser(b *cacheKeyBuilder, u user.Info) { @@ -265,20 +264,21 @@ func addUser(b *cacheKeyBuilder, u user.Info) { }) } -// cacheKeyBuilder adds syntactic sugar on top of cryptobyte.Builder to make it easier to use for complex inputs. +// cacheKeyBuilder builds a binary key from structured inputs using uint32 length-prefixed encoding, then hashes +// it with SHA-256. All methods operate on a single shared buffer to avoid closure and child-builder allocations. type cacheKeyBuilder struct { namespace string // in the programming sense, not the Kubernetes concept - builder *cryptobyte.Builder + builder []byte } func newCacheKeyBuilder(namespace string) *cacheKeyBuilder { // start with a reasonable size to avoid too many allocations - return &cacheKeyBuilder{namespace: namespace, builder: cryptobyte.NewBuilder(make([]byte, 0, 384))} + return &cacheKeyBuilder{namespace: namespace, builder: make([]byte, 0, 384)} } func (c *cacheKeyBuilder) addString(value string) *cacheKeyBuilder { c.addLengthPrefixed(func(c *cacheKeyBuilder) { - c.builder.AddBytes([]byte(value)) + c.builder = append(c.builder, value...) }) return c } @@ -297,24 +297,36 @@ func (c *cacheKeyBuilder) addBool(value bool) *cacheKeyBuilder { if value { b = 1 } - c.builder.AddUint8(b) + c.builder = append(c.builder, b) return c } type builderContinuation func(child *cacheKeyBuilder) func (c *cacheKeyBuilder) addLengthPrefixed(f builderContinuation) { - c.builder.AddUint32LengthPrefixed(func(b *cryptobyte.Builder) { - c := &cacheKeyBuilder{namespace: c.namespace, builder: b} - f(c) - }) + offset := len(c.builder) + c.builder = append(c.builder, 0, 0, 0, 0) // placeholder for uint32 length + f(c) + binary.BigEndian.PutUint32(c.builder[offset:], uint32(len(c.builder)-offset-4)) } -func (c *cacheKeyBuilder) build() (string, error) { - key, err := c.builder.Bytes() - if err != nil { - return "", err - } - hash := sha256.Sum256(key) // reduce the size of the cache key to keep the overall cache size small - return fmt.Sprintf("%x/%s", hash[:], c.namespace), nil +func (c *cacheKeyBuilder) build() string { + hashed := sha256.Sum256(c.builder) // reduce the size of the cache key to keep the overall cache size small + // sha256 = 32 bytes -> 64 hex chars + "/" + namespace + buf := make([]byte, 0, 64+1+len(c.namespace)) + buf = hex.AppendEncode(buf, hashed[:]) + buf = append(buf, '/') + buf = append(buf, c.namespace...) + return toString(buf) // only safe because buf is heap allocated via make +} + +// toString performs unholy acts to avoid allocations +func toString(b []byte) string { + // unsafe.SliceData relies on cap whereas we want to rely on len + if len(b) == 0 { + return "" + } + // Copied from go 1.20.1 strings.Builder.String + // https://github.com/golang/go/blob/202a1a57064127c3f19d96df57b9f9586145e21c/src/strings/builder.go#L48 + return unsafe.String(unsafe.SliceData(b), len(b)) } 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 5069baa0b03..80e771daefe 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 @@ -18,8 +18,10 @@ package impersonation import ( "context" + "crypto/sha256" "encoding/json" "fmt" + "hash/fnv" "io" "net/http" "net/http/httptest" @@ -47,7 +49,7 @@ import ( ) type constrainedImpersonationTest struct { - t *testing.T + t testing.TB constrainedImpersonationHandler *constrainedImpersonationHandler authzChecks []authzCheck @@ -106,6 +108,10 @@ func (c *constrainedImpersonationTest) Authorize(ctx context.Context, a authoriz return authorizer.DecisionAllow, "", nil } + if u.GetName() == "user-impersonater" && a.GetVerb() == "impersonate:user-info" && a.GetResource() == "uids" { + return authorizer.DecisionAllow, "", nil + } + if len(u.GetGroups()) > 0 && u.GetGroups()[0] == "group-impersonater" && a.GetVerb() == "impersonate:user-info" && a.GetResource() == "groups" { return authorizer.DecisionAllow, "", nil } @@ -450,12 +456,12 @@ func (c *constrainedImpersonationTest) assertCache(r testRequest) { } rr.Equal(len(expectedMode.outer), outer.cache.Len()) for expectedKey, expectedUser := range expectedMode.outer { - c.checkCacheEntry(outer, expectedKey.wantedUser, expectedKey.attributes, expectedUser, verb) + c.checkCacheEntry(outer, expectedKey.wantedUser, expectedKey.attributes, expectedKey.rawKey, expectedUser, verb) } rr.Equal(len(expectedMode.inner), inner.cache.Len()) for expectedKey, expectedUser := range expectedMode.inner { - c.checkCacheEntry(inner, expectedKey.wantedUser, authorizer.AttributesRecord{User: expectedKey.requestor}, expectedUser, verb) + c.checkCacheEntry(inner, expectedKey.wantedUser, authorizer.AttributesRecord{User: expectedKey.requestor}, expectedKey.rawKey, expectedUser, verb) } } } @@ -465,7 +471,14 @@ func usernameHash(username string) string { return fmt.Sprintf("%x", hash) } -func (c *constrainedImpersonationTest) checkCacheEntry(cache *impersonationCache, wantedUser *user.DefaultInfo, attributes authorizer.Attributes, expectedUser *user.DefaultInfo, expectedVerb string) { +func fnvSum128a(data []byte) []byte { + h := fnv.New128a() + h.Write(data) + var sum [16]byte + return h.Sum(sum[:0]) +} + +func (c *constrainedImpersonationTest) checkCacheEntry(cache *impersonationCache, wantedUser *user.DefaultInfo, attributes authorizer.Attributes, rawKey string, expectedUser *user.DefaultInfo, expectedVerb string) { rr := require.New(c.t) keyString, err := buildKey(wantedUser, attributes) rr.NoError(err) @@ -474,7 +487,12 @@ func (c *constrainedImpersonationTest) checkCacheEntry(cache *impersonationCache impersonatedUser := val.(*impersonatedUserInfo) rr.Equal(expectedVerb, impersonatedUser.constraint) rr.Equal(expectedUser, impersonatedUser.user) - rr.True(strings.HasSuffix(keyString, "/"+attributes.GetUser().GetName())) + rr.Equal(keyHash(rawKey)+"/"+attributes.GetUser().GetName(), keyString, "hash of %q does not match", rawKey) +} + +func keyHash(rawKey string) string { + hash := sha256.Sum256([]byte(rawKey)) + return fmt.Sprintf("%x", hash[:]) } func withConstrainedImpersonationAttributes(a authorizer.AttributesRecord, mode string) authorizer.AttributesRecord { @@ -527,11 +545,12 @@ func expectDeny(attributes authorizer.AttributesRecord) authzCheck { return expectAuthzCheck(attributes, authorizer.DecisionNoOpinion) } -func outerCacheKey(wantedUser, requestor *user.DefaultInfo, req *request.RequestInfo) impersonationCacheKey { +func outerCacheKey(wantedUser, requestor *user.DefaultInfo, req *request.RequestInfo, rawKey string) outerKey { attributes := withUser(requestInfoToAttributes(req), requestor) - return impersonationCacheKey{ + return outerKey{ wantedUser: wantedUser, attributes: &attributes, // use a *authorizer.AttributesRecord so that it can be used in a map key + rawKey: rawKey, } } @@ -541,12 +560,19 @@ type expectedCache struct { } type expectedModeCache struct { - outer map[impersonationCacheKey]*user.DefaultInfo + outer map[outerKey]*user.DefaultInfo inner map[innerKey]*user.DefaultInfo } +type outerKey struct { + wantedUser *user.DefaultInfo + attributes *authorizer.AttributesRecord + rawKey string +} + type innerKey struct { wantedUser, requestor *user.DefaultInfo + rawKey string } type testRequest struct { @@ -619,13 +645,16 @@ func associatedNodeTestCase() []testRequest { }, modes: map[string]expectedModeCache{ "impersonate:associated-node": { - outer: map[impersonationCacheKey]*user.DefaultInfo{ - outerCacheKey(&user.DefaultInfo{Name: "system:node:*"}, saDefaultOnAnyNode, getSecretRequest): node1FullUserInfo, + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(&user.DefaultInfo{Name: "system:node:*"}, saDefaultOnAnyNode, getSecretRequest, + "\x00\x00\x00\x1d\x00\x00\x00\rsystem:node:*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb7\x00\x00\x00%system:serviceaccount:default:default\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x1bassociate-node-impersonater\x00\x00\x00c\x00\x00\x001authentication.kubernetes.io/associated-node-keys\x00\x00\x00*\x00\x00\x00&authentication.kubernetes.io/node-name\x00\x00\x004\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\asecrets\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): node1FullUserInfo, }, inner: map[innerKey]*user.DefaultInfo{ { wantedUser: &user.DefaultInfo{Name: "system:node:*"}, requestor: saDefaultOnAnyNode, + rawKey: "\x00\x00\x00\x1d\x00\x00\x00\rsystem:node:*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb7\x00\x00\x00%system:serviceaccount:default:default\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x1bassociate-node-impersonater\x00\x00\x00c\x00\x00\x001authentication.kubernetes.io/associated-node-keys\x00\x00\x00*\x00\x00\x00&authentication.kubernetes.io/node-name\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", }: node1FullUserInfo, }, }, @@ -635,10 +664,14 @@ func associatedNodeTestCase() []testRequest { modeIdx: cacheWithOnlyNode1Data.modeIdx, modes: map[string]expectedModeCache{ "impersonate:associated-node": { - outer: map[impersonationCacheKey]*user.DefaultInfo{ - outerCacheKey(&user.DefaultInfo{Name: "system:node:*"}, saDefaultOnAnyNode, getSecretRequest): node1FullUserInfo, + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(&user.DefaultInfo{Name: "system:node:*"}, saDefaultOnAnyNode, getSecretRequest, + "\x00\x00\x00\x1d\x00\x00\x00\rsystem:node:*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb7\x00\x00\x00%system:serviceaccount:default:default\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x1bassociate-node-impersonater\x00\x00\x00c\x00\x00\x001authentication.kubernetes.io/associated-node-keys\x00\x00\x00*\x00\x00\x00&authentication.kubernetes.io/node-name\x00\x00\x004\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\asecrets\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): node1FullUserInfo, // even though this request was made on node2, since the inner cache matched it returned a value for node1 - outerCacheKey(&user.DefaultInfo{Name: "system:node:*"}, saDefaultOnAnyNode, getPodRequest): node1FullUserInfo, + outerCacheKey(&user.DefaultInfo{Name: "system:node:*"}, saDefaultOnAnyNode, getPodRequest, + "\x00\x00\x00\x1d\x00\x00\x00\rsystem:node:*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb7\x00\x00\x00%system:serviceaccount:default:default\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x1bassociate-node-impersonater\x00\x00\x00c\x00\x00\x001authentication.kubernetes.io/associated-node-keys\x00\x00\x00*\x00\x00\x00&authentication.kubernetes.io/node-name\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): node1FullUserInfo, }, inner: cacheWithOnlyNode1Data.modes["impersonate:associated-node"].inner, }, @@ -730,7 +763,12 @@ func associatedNodeTestCase() []testRequest { } } -func TestConstrainedImpersonationFilter(t *testing.T) { +type impersonationTestCase struct { + name string + requests []testRequest +} + +func constrainedImpersonationTestCases() []impersonationTestCase { getPodRequest := &request.RequestInfo{ IsResourceRequest: true, Verb: "get", @@ -784,10 +822,7 @@ func TestConstrainedImpersonationFilter(t *testing.T) { saImpersonator := &user.DefaultInfo{Name: "sa-impersonater"} nodeImpersonator := &user.DefaultInfo{Name: "node-impersonater"} - testCases := []struct { - name string - requests []testRequest - }{ + testCases := []impersonationTestCase{ { name: "impersonating-error", requests: []testRequest{ @@ -831,11 +866,17 @@ func TestConstrainedImpersonationFilter(t *testing.T) { }, modes: map[string]expectedModeCache{ "impersonate:user-info": { - outer: map[impersonationCacheKey]*user.DefaultInfo{ - outerCacheKey(anyone, userImpersonator, getPodRequest): anyoneAuthenticated, + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(anyone, userImpersonator, getPodRequest, + "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): anyoneAuthenticated, }, inner: map[innerKey]*user.DefaultInfo{ - {wantedUser: anyone, requestor: userImpersonator}: anyoneAuthenticated, + { + wantedUser: anyone, + requestor: userImpersonator, + rawKey: "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: anyoneAuthenticated, }, }, }, @@ -861,12 +902,20 @@ func TestConstrainedImpersonationFilter(t *testing.T) { }, modes: map[string]expectedModeCache{ "impersonate:user-info": { - outer: map[impersonationCacheKey]*user.DefaultInfo{ - outerCacheKey(anyone, userImpersonator, getPodRequest): anyoneAuthenticated, - outerCacheKey(anyone, userImpersonator, getAnotherPodRequest): anyoneAuthenticated, + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(anyone, userImpersonator, getPodRequest, + "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): anyoneAuthenticated, + outerCacheKey(anyone, userImpersonator, getAnotherPodRequest, + "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x003\x00\x00\x00\x03get\x01\x00\x00\x00\x04bar1\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x04foo1\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): anyoneAuthenticated, }, inner: map[innerKey]*user.DefaultInfo{ - {wantedUser: anyone, requestor: userImpersonator}: anyoneAuthenticated, + { + wantedUser: anyone, + requestor: userImpersonator, + rawKey: "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: anyoneAuthenticated, }, }, }, @@ -892,12 +941,20 @@ func TestConstrainedImpersonationFilter(t *testing.T) { }, modes: map[string]expectedModeCache{ "impersonate:user-info": { - outer: map[impersonationCacheKey]*user.DefaultInfo{ - outerCacheKey(anyone, userImpersonator, getPodRequest): anyoneAuthenticated, - outerCacheKey(anyone, userImpersonator, getAnotherPodRequest): anyoneAuthenticated, + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(anyone, userImpersonator, getPodRequest, + "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): anyoneAuthenticated, + outerCacheKey(anyone, userImpersonator, getAnotherPodRequest, + "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x003\x00\x00\x00\x03get\x01\x00\x00\x00\x04bar1\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x04foo1\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): anyoneAuthenticated, }, inner: map[innerKey]*user.DefaultInfo{ - {wantedUser: anyone, requestor: userImpersonator}: anyoneAuthenticated, + { + wantedUser: anyone, + requestor: userImpersonator, + rawKey: "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: anyoneAuthenticated, }, }, }, @@ -931,13 +988,16 @@ func TestConstrainedImpersonationFilter(t *testing.T) { }, modes: map[string]expectedModeCache{ "impersonate:serviceaccount": { - outer: map[impersonationCacheKey]*user.DefaultInfo{ - outerCacheKey(saUser, saImpersonator, getPodRequest): saUserAuthenticated, + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(saUser, saImpersonator, getPodRequest, + "\x00\x00\x005\x00\x00\x00%system:serviceaccount:default:default\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x0fsa-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): saUserAuthenticated, }, inner: map[innerKey]*user.DefaultInfo{ { wantedUser: saUser, requestor: saImpersonator, + rawKey: "\x00\x00\x005\x00\x00\x00%system:serviceaccount:default:default\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x0fsa-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", }: saUserAuthenticated, }, }, @@ -1018,11 +1078,17 @@ func TestConstrainedImpersonationFilter(t *testing.T) { }, modes: map[string]expectedModeCache{ "impersonate:arbitrary-node": { - outer: map[impersonationCacheKey]*user.DefaultInfo{ - outerCacheKey(nodeUser, nodeImpersonator, getPodRequest): nodeUserAuthenticated, + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(nodeUser, nodeImpersonator, getPodRequest, + "\x00\x00\x00!\x00\x00\x00\x11system:node:node1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11node-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): nodeUserAuthenticated, }, inner: map[innerKey]*user.DefaultInfo{ - {wantedUser: nodeUser, requestor: nodeImpersonator}: nodeUserAuthenticated, + { + wantedUser: nodeUser, + requestor: nodeImpersonator, + rawKey: "\x00\x00\x00!\x00\x00\x00\x11system:node:node1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11node-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: nodeUserAuthenticated, }, }, }, @@ -1106,7 +1172,7 @@ func TestConstrainedImpersonationFilter(t *testing.T) { }, modes: map[string]expectedModeCache{ "impersonate:user-info": { - outer: map[impersonationCacheKey]*user.DefaultInfo{ + outer: map[outerKey]*user.DefaultInfo{ outerCacheKey( &user.DefaultInfo{ Name: "system:admin", @@ -1116,7 +1182,7 @@ func TestConstrainedImpersonationFilter(t *testing.T) { Name: "user-impersonater", Groups: []string{"extra-setter-scopes"}, }, - getPodRequest): { + getPodRequest, "\x00\x00\x00c\x00\x00\x00\fsystem:admin\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00G\x00\x00\x00\x10pandas.io/scopes\x00\x00\x00/\x00\x00\x00\ascope-a\x00\x00\x00\ascope-b\x00\x00\x00\x015\x00\x00\x00\x014\x00\x00\x00\x013\x00\x00\x00\x012\x00\x00\x00\x011\x00\x00\x008\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x17\x00\x00\x00\x13extra-setter-scopes\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"): { Name: "system:admin", Groups: []string{"system:authenticated"}, Extra: map[string][]string{"pandas.io/scopes": {"scope-a", "scope-b", "5", "4", "3", "2", "1"}}, @@ -1132,6 +1198,7 @@ func TestConstrainedImpersonationFilter(t *testing.T) { Name: "user-impersonater", Groups: []string{"extra-setter-scopes"}, }, + rawKey: "\x00\x00\x00c\x00\x00\x00\fsystem:admin\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00G\x00\x00\x00\x10pandas.io/scopes\x00\x00\x00/\x00\x00\x00\ascope-a\x00\x00\x00\ascope-b\x00\x00\x00\x015\x00\x00\x00\x014\x00\x00\x00\x013\x00\x00\x00\x012\x00\x00\x00\x011\x00\x00\x008\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x17\x00\x00\x00\x13extra-setter-scopes\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", }: { Name: "system:admin", Groups: []string{"system:authenticated"}, @@ -1273,11 +1340,13 @@ func TestConstrainedImpersonationFilter(t *testing.T) { }, modes: map[string]expectedModeCache{ "impersonate:user-info": { - outer: map[impersonationCacheKey]*user.DefaultInfo{ + outer: map[outerKey]*user.DefaultInfo{ outerCacheKey(&user.DefaultInfo{ Name: "bob", Groups: []string{"group1", "group2", "group3", "group4"}, - }, &user.DefaultInfo{Name: "many-groups-impersonater"}, getPodRequest): { + }, &user.DefaultInfo{Name: "many-groups-impersonater"}, getPodRequest, + "\x00\x00\x00;\x00\x00\x00\x03bob\x00\x00\x00\x00\x00\x00\x00(\x00\x00\x00\x06group1\x00\x00\x00\x06group2\x00\x00\x00\x06group3\x00\x00\x00\x06group4\x00\x00\x00\x00\x00\x00\x00(\x00\x00\x00\x18many-groups-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): { Name: "bob", Groups: []string{"group1", "group2", "group3", "group4", "system:authenticated"}, }, @@ -1289,6 +1358,7 @@ func TestConstrainedImpersonationFilter(t *testing.T) { Groups: []string{"group1", "group2", "group3", "group4"}, }, requestor: &user.DefaultInfo{Name: "many-groups-impersonater"}, + rawKey: "\x00\x00\x00;\x00\x00\x00\x03bob\x00\x00\x00\x00\x00\x00\x00(\x00\x00\x00\x06group1\x00\x00\x00\x06group2\x00\x00\x00\x06group3\x00\x00\x00\x06group4\x00\x00\x00\x00\x00\x00\x00(\x00\x00\x00\x18many-groups-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", }: { Name: "bob", Groups: []string{"group1", "group2", "group3", "group4", "system:authenticated"}, @@ -1344,7 +1414,7 @@ func TestConstrainedImpersonationFilter(t *testing.T) { }, modes: map[string]expectedModeCache{ "impersonate:user-info": { - outer: map[impersonationCacheKey]*user.DefaultInfo{ + outer: map[outerKey]*user.DefaultInfo{ outerCacheKey(&user.DefaultInfo{ Name: "alice", Extra: map[string][]string{ @@ -1353,7 +1423,9 @@ func TestConstrainedImpersonationFilter(t *testing.T) { "scopes.example.com/key3": {"val3"}, "scopes.example.com/key4": {"val4"}, }, - }, &user.DefaultInfo{Name: "many-extras-impersonater"}, getPodRequest): { + }, &user.DefaultInfo{Name: "many-extras-impersonater"}, getPodRequest, + "\x00\x00\x00\xb1\x00\x00\x00\x05alice\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x9c\x00\x00\x00\x17scopes.example.com/key1\x00\x00\x00\b\x00\x00\x00\x04val1\x00\x00\x00\x17scopes.example.com/key2\x00\x00\x00\b\x00\x00\x00\x04val2\x00\x00\x00\x17scopes.example.com/key3\x00\x00\x00\b\x00\x00\x00\x04val3\x00\x00\x00\x17scopes.example.com/key4\x00\x00\x00\b\x00\x00\x00\x04val4\x00\x00\x00(\x00\x00\x00\x18many-extras-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): { Name: "alice", Groups: []string{"system:authenticated"}, Extra: map[string][]string{ @@ -1376,6 +1448,7 @@ func TestConstrainedImpersonationFilter(t *testing.T) { }, }, requestor: &user.DefaultInfo{Name: "many-extras-impersonater"}, + rawKey: "\x00\x00\x00\xb1\x00\x00\x00\x05alice\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x9c\x00\x00\x00\x17scopes.example.com/key1\x00\x00\x00\b\x00\x00\x00\x04val1\x00\x00\x00\x17scopes.example.com/key2\x00\x00\x00\b\x00\x00\x00\x04val2\x00\x00\x00\x17scopes.example.com/key3\x00\x00\x00\b\x00\x00\x00\x04val3\x00\x00\x00\x17scopes.example.com/key4\x00\x00\x00\b\x00\x00\x00\x04val4\x00\x00\x00(\x00\x00\x00\x18many-extras-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", }: { Name: "alice", Groups: []string{"system:authenticated"}, @@ -1444,14 +1517,16 @@ func TestConstrainedImpersonationFilter(t *testing.T) { }, modes: map[string]expectedModeCache{ "impersonate:user-info": { - outer: map[impersonationCacheKey]*user.DefaultInfo{ + outer: map[outerKey]*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): { + }, &user.DefaultInfo{Name: "mixed-impersonater"}, getPodRequest, + "\x00\x00\x00e\x00\x00\x00\x05frank\x00\x00\x00\x00\x00\x00\x00#\x00\x00\x00\x03dev\x00\x00\x00\x03ops\x00\x00\x00\bsecurity\x00\x00\x00\x05audit\x00\x00\x00-\x00\x00\x00\vtags.io/env\x00\x00\x00\x1a\x00\x00\x00\x04prod\x00\x00\x00\astaging\x00\x00\x00\x03dev\x00\x00\x00\"\x00\x00\x00\x12mixed-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): { Name: "frank", Groups: []string{"dev", "ops", "security", "audit", "system:authenticated"}, Extra: map[string][]string{ @@ -1469,6 +1544,7 @@ func TestConstrainedImpersonationFilter(t *testing.T) { }, }, requestor: &user.DefaultInfo{Name: "mixed-impersonater"}, + rawKey: "\x00\x00\x00e\x00\x00\x00\x05frank\x00\x00\x00\x00\x00\x00\x00#\x00\x00\x00\x03dev\x00\x00\x00\x03ops\x00\x00\x00\bsecurity\x00\x00\x00\x05audit\x00\x00\x00-\x00\x00\x00\vtags.io/env\x00\x00\x00\x1a\x00\x00\x00\x04prod\x00\x00\x00\astaging\x00\x00\x00\x03dev\x00\x00\x00\"\x00\x00\x00\x12mixed-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", }: { Name: "frank", Groups: []string{"dev", "ops", "security", "audit", "system:authenticated"}, @@ -1511,14 +1587,20 @@ func TestConstrainedImpersonationFilter(t *testing.T) { }, modes: map[string]expectedModeCache{ "impersonate:user-info": { - outer: map[impersonationCacheKey]*user.DefaultInfo{ - outerCacheKey(&user.DefaultInfo{Name: "bob"}, userImpersonator, getDeploymentRequest): { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(&user.DefaultInfo{Name: "bob"}, userImpersonator, getDeploymentRequest, + "\x00\x00\x00\x13\x00\x00\x00\x03bob\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00<\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\vdeployments\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x04apps\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): { Name: "bob", Groups: []string{"system:authenticated"}, }, }, inner: map[innerKey]*user.DefaultInfo{ - {wantedUser: &user.DefaultInfo{Name: "bob"}, requestor: userImpersonator}: { + { + wantedUser: &user.DefaultInfo{Name: "bob"}, + requestor: userImpersonator, + rawKey: "\x00\x00\x00\x13\x00\x00\x00\x03bob\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: { Name: "bob", Groups: []string{"system:authenticated"}, }, @@ -1549,14 +1631,20 @@ func TestConstrainedImpersonationFilter(t *testing.T) { }, modes: map[string]expectedModeCache{ "impersonate:user-info": { - outer: map[impersonationCacheKey]*user.DefaultInfo{ - outerCacheKey(&user.DefaultInfo{Name: "bob"}, userImpersonator, getDeploymentRequest): { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(&user.DefaultInfo{Name: "bob"}, userImpersonator, getDeploymentRequest, + "\x00\x00\x00\x13\x00\x00\x00\x03bob\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00<\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\vdeployments\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x04apps\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): { Name: "bob", Groups: []string{"system:authenticated"}, }, }, inner: map[innerKey]*user.DefaultInfo{ - {wantedUser: &user.DefaultInfo{Name: "bob"}, requestor: userImpersonator}: { + { + wantedUser: &user.DefaultInfo{Name: "bob"}, + requestor: userImpersonator, + rawKey: "\x00\x00\x00\x13\x00\x00\x00\x03bob\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: { Name: "bob", Groups: []string{"system:authenticated"}, }, @@ -1570,7 +1658,314 @@ func TestConstrainedImpersonationFilter(t *testing.T) { }, }, }, + { + name: "impersonating-user-with-uid", + requests: []testRequest{ + { + request: getPodRequest, + requestor: &user.DefaultInfo{Name: "user-impersonater", UID: "requestor-uid-123"}, + impersonatedUser: &user.DefaultInfo{ + Name: "anyone", + UID: "wanted-uid-456", + }, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "anyone", + UID: "wanted-uid-456", + Groups: []string{"system:authenticated"}, + }, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "uids", Name: "wanted-uid-456"}, "user-info")), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + "user-impersonater": "impersonate:user-info", + }, + modes: map[string]expectedModeCache{ + "impersonate:user-info": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey( + &user.DefaultInfo{Name: "anyone", UID: "wanted-uid-456"}, + &user.DefaultInfo{Name: "user-impersonater", UID: "requestor-uid-123"}, + getPodRequest, + "\x00\x00\x00$\x00\x00\x00\x06anyone\x00\x00\x00\x0ewanted-uid-456\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x002\x00\x00\x00\x11user-impersonater\x00\x00\x00\x11requestor-uid-123\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): { + Name: "anyone", + UID: "wanted-uid-456", + Groups: []string{"system:authenticated"}, + }, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: &user.DefaultInfo{Name: "anyone", UID: "wanted-uid-456"}, + requestor: &user.DefaultInfo{Name: "user-impersonater", UID: "requestor-uid-123"}, + rawKey: "\x00\x00\x00$\x00\x00\x00\x06anyone\x00\x00\x00\x0ewanted-uid-456\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x002\x00\x00\x00\x11user-impersonater\x00\x00\x00\x11requestor-uid-123\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: { + Name: "anyone", + UID: "wanted-uid-456", + Groups: []string{"system:authenticated"}, + }, + }, + }, + }, + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "user-info", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 3, // impersonate-on + impersonate:user-info users + uids + }, + }, + }, + }, + { + name: "impersonating-user-subresource-request", + requests: []testRequest{ + { + request: &request.RequestInfo{ + IsResourceRequest: true, + Verb: "get", + APIVersion: "v1", + Resource: "pods", + Subresource: "status", + Name: "foo", + Namespace: "bar", + }, + requestor: userImpersonator, + impersonatedUser: anyone, + expectedImpersonatedUser: anyoneAuthenticated, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(&request.RequestInfo{ + IsResourceRequest: true, + Verb: "get", + APIVersion: "v1", + Resource: "pods", + Subresource: "status", + Name: "foo", + Namespace: "bar", + }, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"}, "user-info")), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + userImpersonator.Name: "impersonate:user-info", + }, + modes: map[string]expectedModeCache{ + "impersonate:user-info": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(anyone, userImpersonator, &request.RequestInfo{ + IsResourceRequest: true, + Verb: "get", + APIVersion: "v1", + Resource: "pods", + Subresource: "status", + Name: "foo", + Namespace: "bar", + }, + "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x007\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x06status\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): anyoneAuthenticated, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: anyone, + requestor: userImpersonator, + rawKey: "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: anyoneAuthenticated, + }, + }, + }, + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "user-info", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 2, // impersonate-on + impersonate:user-info + }, + }, + }, + }, + { + name: "impersonating-user-non-resource-request", + requests: []testRequest{ + { + request: &request.RequestInfo{ + IsResourceRequest: false, + Verb: "get", + Path: "/healthz", + }, + requestor: userImpersonator, + impersonatedUser: anyone, + expectedImpersonatedUser: anyoneAuthenticated, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(&request.RequestInfo{ + IsResourceRequest: false, + Verb: "get", + Path: "/healthz", + }, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"}, "user-info")), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + userImpersonator.Name: "impersonate:user-info", + }, + modes: map[string]expectedModeCache{ + "impersonate:user-info": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(anyone, userImpersonator, &request.RequestInfo{ + IsResourceRequest: false, + Verb: "get", + Path: "/healthz", + }, + "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00\x00\x00\x03get\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\b/healthz\x00\x00\x00\x00\x00\x00\x00\x00", + ): anyoneAuthenticated, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: anyone, + requestor: userImpersonator, + rawKey: "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: anyoneAuthenticated, + }, + }, + }, + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "user-info", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 2, // impersonate-on + impersonate:user-info + }, + }, + }, + }, + { + name: "impersonating-user-with-field-and-label-selectors", + requests: []testRequest{ + { + request: &request.RequestInfo{ + IsResourceRequest: true, + Verb: "list", + APIVersion: "v1", + Resource: "pods", + Namespace: "bar", + FieldSelector: "spec.nodeName=node1", + LabelSelector: "app=web", + }, + requestor: userImpersonator, + impersonatedUser: anyone, + expectedImpersonatedUser: anyoneAuthenticated, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(&request.RequestInfo{ + IsResourceRequest: true, + Verb: "list", + APIVersion: "v1", + Resource: "pods", + Namespace: "bar", + FieldSelector: "spec.nodeName=node1", + LabelSelector: "app=web", + }, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"}, "user-info")), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + userImpersonator.Name: "impersonate:user-info", + }, + modes: map[string]expectedModeCache{ + "impersonate:user-info": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(anyone, userImpersonator, &request.RequestInfo{ + IsResourceRequest: true, + Verb: "list", + APIVersion: "v1", + Resource: "pods", + Namespace: "bar", + FieldSelector: "spec.nodeName=node1", + LabelSelector: "app=web", + }, "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00/\x00\x00\x00\x04list\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00#\x00\x00\x00\x1f\x00\x00\x00\rspec.nodeName\x00\x00\x00\x01=\x00\x00\x00\x05node1\x00\x00\x00\v\x00\x00\x00\aapp=web"): anyoneAuthenticated, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: anyone, + requestor: userImpersonator, + rawKey: "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: anyoneAuthenticated, + }, + }, + }, + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "user-info", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 2, // impersonate-on + impersonate:user-info + }, + }, + }, + }, + { + name: "legacy-fallback", + requests: []testRequest{ + { + request: getPodRequest, + requestor: &user.DefaultInfo{Name: "legacy-impersonater"}, + impersonatedUser: anyone, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "anyone", + Groups: []string{"system:authenticated"}, + }, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + expectDeny(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"}, "user-info")), + expectAllow(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"})), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + "legacy-impersonater": "impersonate", + }, + modes: nil, // legacy impersonation does not cache + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "legacy", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 1, // impersonate-on:user-info:get + "user-info/denied": 1, // impersonate:user-info users + "legacy/allowed": 1, // impersonate users + }, + }, + { + request: getDeploymentRequest, + requestor: &user.DefaultInfo{Name: "legacy-impersonater"}, + impersonatedUser: anyone, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "anyone", + Groups: []string{"system:authenticated"}, + }, + expectedAuthzChecks: []authzCheck{ + expectAllow(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"})), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + "legacy-impersonater": "impersonate", + }, + modes: nil, // legacy impersonation does not cache + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "legacy", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "legacy/allowed": 1, // impersonate users (mode index cache hit skips constrained checks) + }, + }, + }, + }, } + return testCases +} + +func TestConstrainedImpersonationFilter(t *testing.T) { + testCases := constrainedImpersonationTestCases() var mux http.ServeMux tests := make([]*constrainedImpersonationTest, len(testCases)) @@ -1593,28 +1988,7 @@ func TestConstrainedImpersonationFilter(t *testing.T) { handlers[i] = test.handler() for _, r := range tc.requests { - client := &http.Client{ - Transport: &testRoundTripper{ - user: r.requestor, - requestInfo: r.request, - delegate: transport.NewImpersonatingRoundTripper( - transport.ImpersonationConfig{ - UserName: r.impersonatedUser.Name, - UID: r.impersonatedUser.UID, - Groups: r.impersonatedUser.Groups, - Extra: r.impersonatedUser.Extra, - }, - http.DefaultTransport, - ), - }, - } - - req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, server.URL+"/"+strconv.Itoa(i), nil) - require.NoError(t, err) - - resp, err := client.Do(req) - require.NoError(t, err) - require.Equal(t, r.expectedCode, resp.StatusCode) + resp := doImpersonationRequest(t, http.DefaultTransport, server.URL+"/"+strconv.Itoa(i), r, r.expectedCode) body, err := io.ReadAll(resp.Body) require.NoError(t, err) @@ -1932,3 +2306,101 @@ func TestConstrainedImpersonationParallel(t *testing.T) { }) } } + +func (c *constrainedImpersonationTest) benchmarkHandler(withImpersonation func(http.Handler, authorizer.Authorizer, runtime.NegotiatedSerializer) http.Handler) http.Handler { + s := runtime.NewScheme() + metav1.AddToGroupVersion(s, metav1.SchemeGroupVersion) + addImpersonation := withImpersonation(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {}), c, serializer.NewCodecFactory(s)) + h := addImpersonation.(*constrainedImpersonationHandler) + h.recordAttempt = func(ctx context.Context, mode, decision string, _ time.Duration) {} + h.metricsAuthorizer.recordAuthorizationCall = func(mode, decision string, _ time.Duration) {} + addAuthentication := c.authenticationHandler(addImpersonation) + addRequestInfo := c.requestInfoHandler(addAuthentication) + return addRequestInfo +} + +func BenchmarkImpersonation(b *testing.B) { + testCases := constrainedImpersonationTestCases() + + type impersonationHandler struct { + name string + fn func(http.Handler, authorizer.Authorizer, runtime.NegotiatedSerializer) http.Handler + isLegacy bool + } + + impersonators := []impersonationHandler{ + { + name: "constrained", + fn: WithConstrainedImpersonation, + }, + { + name: "legacy", + fn: WithImpersonation, + isLegacy: true, + }, + } + + for _, imp := range impersonators { + b.Run(imp.name, func(b *testing.B) { + for _, tc := range testCases { + b.Run(tc.name, func(b *testing.B) { + test := &constrainedImpersonationTest{t: b} + handler := test.benchmarkHandler(imp.fn) + + server := httptest.NewServer(handler) + defer server.Close() + + baseTransport := &http.Transport{ + DisableKeepAlives: true, + } + + b.ResetTimer() + for b.Loop() { + for _, r := range tc.requests { + resp := doImpersonationRequest(b, baseTransport, server.URL, r, benchmarkExpectedCode(r, imp.isLegacy)) + _ = resp.Body.Close() + } + } + b.StopTimer() + }) + } + }) + } +} + +func benchmarkExpectedCode(r testRequest, isLegacy bool) int { + if isLegacy && r.expectedCode == http.StatusOK && r.requestor.Name != "legacy-impersonater" { + // legacy WithImpersonation denies any requestor that only has constrained impersonation verbs + return http.StatusForbidden + } + return r.expectedCode +} + +func doImpersonationRequest(tb testing.TB, baseTransport http.RoundTripper, url string, r testRequest, expectedCode int) *http.Response { + tb.Helper() + + client := &http.Client{ + Transport: &testRoundTripper{ + user: r.requestor, + requestInfo: r.request, + delegate: transport.NewImpersonatingRoundTripper( + transport.ImpersonationConfig{ + UserName: r.impersonatedUser.Name, + UID: r.impersonatedUser.UID, + Groups: r.impersonatedUser.Groups, + Extra: r.impersonatedUser.Extra, + }, + baseTransport, + ), + }, + } + + req, err := http.NewRequest(http.MethodGet, url, nil) + require.NoError(tb, err) + + resp, err := client.Do(req) + require.NoError(tb, err) + require.Equal(tb, expectedCode, resp.StatusCode) + + return resp +}