diff --git a/pkg/admission/plugin/policy/validating/caching_authorizer.go b/pkg/admission/plugin/policy/validating/caching_authorizer.go index fbefd595e..ac13dbeee 100644 --- a/pkg/admission/plugin/policy/validating/caching_authorizer.go +++ b/pkg/admission/plugin/policy/validating/caching_authorizer.go @@ -22,6 +22,8 @@ import ( "sort" "strings" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authorization/authorizer" ) @@ -58,6 +60,8 @@ var _ authorizer.Attributes = (interface { GetAPIVersion() string IsResourceRequest() bool GetPath() string + GetFieldSelector() (fields.Requirements, error) + GetLabelSelector() (labels.Requirements, error) })(nil) // The user info accessors known to cache key construction. If this fails to compile, the cache @@ -72,16 +76,31 @@ var _ user.Info = (interface { // Authorize returns an authorization decision by delegating to another Authorizer. If an equivalent // check has already been performed, a cached result is returned. Not safe for concurrent use. func (ca *cachingAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { - serializableAttributes := authorizer.AttributesRecord{ - Verb: a.GetVerb(), - Namespace: a.GetNamespace(), - APIGroup: a.GetAPIGroup(), - APIVersion: a.GetAPIVersion(), - Resource: a.GetResource(), - Subresource: a.GetSubresource(), - Name: a.GetName(), - ResourceRequest: a.IsResourceRequest(), - Path: a.GetPath(), + type SerializableAttributes struct { + authorizer.AttributesRecord + LabelSelector string + } + + serializableAttributes := SerializableAttributes{ + AttributesRecord: authorizer.AttributesRecord{ + Verb: a.GetVerb(), + Namespace: a.GetNamespace(), + APIGroup: a.GetAPIGroup(), + APIVersion: a.GetAPIVersion(), + Resource: a.GetResource(), + Subresource: a.GetSubresource(), + Name: a.GetName(), + ResourceRequest: a.IsResourceRequest(), + Path: a.GetPath(), + }, + } + // in the error case, we won't honor this field selector, so the cache doesn't need it. + if fieldSelector, err := a.GetFieldSelector(); len(fieldSelector) > 0 { + serializableAttributes.FieldSelectorRequirements, serializableAttributes.FieldSelectorParsingErr = fieldSelector, err + } + if labelSelector, _ := a.GetLabelSelector(); len(labelSelector) > 0 { + // the labels requirements have private elements so those don't help us serialize to a unique key + serializableAttributes.LabelSelector = labelSelector.String() } if u := a.GetUser(); u != nil { diff --git a/pkg/admission/plugin/policy/validating/caching_authorizer_test.go b/pkg/admission/plugin/policy/validating/caching_authorizer_test.go index da7f219fa..5831cc3fe 100644 --- a/pkg/admission/plugin/policy/validating/caching_authorizer_test.go +++ b/pkg/admission/plugin/policy/validating/caching_authorizer_test.go @@ -18,14 +18,31 @@ package validating import ( "context" + "errors" "fmt" "testing" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authorization/authorizer" + genericfeatures "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + featuregatetesting "k8s.io/component-base/featuregate/testing" ) +func mustParseLabelSelector(str string) labels.Requirements { + ret, err := labels.Parse(str) + if err != nil { + panic(err) + } + retRequirements, _ /*selectable*/ := ret.Requirements() + return retRequirements +} + func TestCachingAuthorizer(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true) + type result struct { decision authorizer.Decision reason string @@ -216,6 +233,261 @@ func TestCachingAuthorizer(t *testing.T) { }, }, }, + { + name: "honor good field selector", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{ + Name: "test name", + FieldSelectorRequirements: fields.ParseSelectorOrDie("foo=bar").Requirements(), + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + { + attributes: authorizer.AttributesRecord{ + Name: "test name", + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason 2", + error: fmt.Errorf("test error 2"), + }, + }, + { + // now this should be cached + attributes: authorizer.AttributesRecord{ + Name: "test name", + FieldSelectorRequirements: fields.ParseSelectorOrDie("foo=bar").Requirements(), + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + { + decision: authorizer.DecisionAllow, + reason: "test reason 2", + error: fmt.Errorf("test error 2"), + }, + }, + }, + { + name: "ignore malformed field selector first", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{ + Name: "test name", + FieldSelectorParsingErr: errors.New("malformed"), + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + { + // notice that this does not have the malformed field selector. + // it should use the cached result + attributes: authorizer.AttributesRecord{ + Name: "test name", + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + { + name: "ignore malformed field selector second", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{ + Name: "test name", + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + { + // this should use the broader cached value because the selector will be ignored + attributes: authorizer.AttributesRecord{ + Name: "test name", + FieldSelectorParsingErr: errors.New("malformed"), + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + + { + name: "honor good label selector", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{ + Name: "test name", + LabelSelectorRequirements: mustParseLabelSelector("foo=bar"), + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + { + attributes: authorizer.AttributesRecord{ + Name: "test name", + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason 2", + error: fmt.Errorf("test error 2"), + }, + }, + { + // now this should be cached + attributes: authorizer.AttributesRecord{ + Name: "test name", + LabelSelectorRequirements: mustParseLabelSelector("foo=bar"), + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + { + attributes: authorizer.AttributesRecord{ + Name: "test name", + LabelSelectorRequirements: mustParseLabelSelector("diff=zero"), + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason 3", + error: fmt.Errorf("test error 3"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + { + decision: authorizer.DecisionAllow, + reason: "test reason 2", + error: fmt.Errorf("test error 2"), + }, + { + decision: authorizer.DecisionAllow, + reason: "test reason 3", + error: fmt.Errorf("test error 3"), + }, + }, + }, + { + name: "ignore malformed label selector first", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{ + Name: "test name", + LabelSelectorParsingErr: errors.New("malformed mess"), + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + { + // notice that this does not have the malformed field selector. + // it should use the cached result + attributes: authorizer.AttributesRecord{ + Name: "test name", + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + { + name: "ignore malformed label selector second", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{ + Name: "test name", + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + { + // this should use the broader cached value because the selector will be ignored + attributes: authorizer.AttributesRecord{ + Name: "test name", + LabelSelectorParsingErr: errors.New("malformed mess"), + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, } { t.Run(tc.name, func(t *testing.T) { var misses int diff --git a/pkg/authorization/authorizer/interfaces.go b/pkg/authorization/authorizer/interfaces.go index 8261c5b58..d39deb17e 100644 --- a/pkg/authorization/authorizer/interfaces.go +++ b/pkg/authorization/authorizer/interfaces.go @@ -20,6 +20,8 @@ import ( "context" "net/http" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apiserver/pkg/authentication/user" ) @@ -62,6 +64,16 @@ type Attributes interface { // GetPath returns the path of the request GetPath() string + + // ParseFieldSelector is lazy, thread-safe, and stores the parsed result and error. + // It returns an error if the field selector cannot be parsed. + // The returned requirements must be treated as readonly and not modified. + GetFieldSelector() (fields.Requirements, error) + + // ParseLabelSelector is lazy, thread-safe, and stores the parsed result and error. + // It returns an error if the label selector cannot be parsed. + // The returned requirements must be treated as readonly and not modified. + GetLabelSelector() (labels.Requirements, error) } // Authorizer makes an authorization decision based on information gained by making @@ -100,6 +112,11 @@ type AttributesRecord struct { Name string ResourceRequest bool Path string + + FieldSelectorRequirements fields.Requirements + FieldSelectorParsingErr error + LabelSelectorRequirements labels.Requirements + LabelSelectorParsingErr error } func (a AttributesRecord) GetUser() user.Info { @@ -146,6 +163,14 @@ func (a AttributesRecord) GetPath() string { return a.Path } +func (a AttributesRecord) GetFieldSelector() (fields.Requirements, error) { + return a.FieldSelectorRequirements, a.FieldSelectorParsingErr +} + +func (a AttributesRecord) GetLabelSelector() (labels.Requirements, error) { + return a.LabelSelectorRequirements, a.LabelSelectorParsingErr +} + type Decision int const ( diff --git a/pkg/endpoints/filters/authorization.go b/pkg/endpoints/filters/authorization.go index e102a1e32..eec02e572 100644 --- a/pkg/endpoints/filters/authorization.go +++ b/pkg/endpoints/filters/authorization.go @@ -22,6 +22,11 @@ import ( "net/http" "time" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + genericfeatures "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/klog/v2" "k8s.io/apimachinery/pkg/runtime" @@ -117,5 +122,31 @@ func GetAuthorizerAttributes(ctx context.Context) (authorizer.Attributes, error) attribs.Namespace = requestInfo.Namespace attribs.Name = requestInfo.Name + if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) { + // parsing here makes it easy to keep the AttributesRecord type value-only and avoids any mutex copies when + // doing shallow copies in other steps. + if len(requestInfo.FieldSelector) > 0 { + fieldSelector, err := fields.ParseSelector(requestInfo.FieldSelector) + if err != nil { + attribs.FieldSelectorRequirements, attribs.FieldSelectorParsingErr = nil, err + } else { + if requirements := fieldSelector.Requirements(); len(requirements) > 0 { + attribs.FieldSelectorRequirements, attribs.FieldSelectorParsingErr = fieldSelector.Requirements(), nil + } + } + } + + if len(requestInfo.LabelSelector) > 0 { + labelSelector, err := labels.Parse(requestInfo.LabelSelector) + if err != nil { + attribs.LabelSelectorRequirements, attribs.LabelSelectorParsingErr = nil, err + } else { + if requirements, _ /*selectable*/ := labelSelector.Requirements(); len(requirements) > 0 { + attribs.LabelSelectorRequirements, attribs.LabelSelectorParsingErr = requirements, nil + } + } + } + } + return &attribs, nil } diff --git a/pkg/endpoints/filters/authorization_test.go b/pkg/endpoints/filters/authorization_test.go index 5840d08b0..deef9054b 100644 --- a/pkg/endpoints/filters/authorization_test.go +++ b/pkg/endpoints/filters/authorization_test.go @@ -19,6 +19,12 @@ package filters import ( "context" "errors" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + genericfeatures "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + featuregatetesting "k8s.io/component-base/featuregate/testing" "net/http" "net/http/httptest" "reflect" @@ -34,10 +40,16 @@ import ( ) func TestGetAuthorizerAttributes(t *testing.T) { + basicLabelRequirement, err := labels.NewRequirement("foo", selection.DoubleEquals, []string{"bar"}) + if err != nil { + t.Fatal(err) + } + testcases := map[string]struct { - Verb string - Path string - ExpectedAttributes *authorizer.AttributesRecord + Verb string + Path string + ExpectedAttributes *authorizer.AttributesRecord + EnableAuthorizationSelector bool }{ "non-resource root": { Verb: "POST", @@ -102,26 +114,122 @@ func TestGetAuthorizerAttributes(t *testing.T) { Resource: "jobs", }, }, + "disabled, ignore good field selector": { + Verb: "GET", + Path: "/apis/batch/v1/namespaces/myns/jobs?fieldSelector%=foo%3Dbar", + ExpectedAttributes: &authorizer.AttributesRecord{ + Verb: "list", + Path: "/apis/batch/v1/namespaces/myns/jobs", + ResourceRequest: true, + APIGroup: batch.GroupName, + APIVersion: "v1", + Namespace: "myns", + Resource: "jobs", + }, + }, + "enabled, good field selector": { + Verb: "GET", + Path: "/apis/batch/v1/namespaces/myns/jobs?fieldSelector=foo%3D%3Dbar", + ExpectedAttributes: &authorizer.AttributesRecord{ + Verb: "list", + Path: "/apis/batch/v1/namespaces/myns/jobs", + ResourceRequest: true, + APIGroup: batch.GroupName, + APIVersion: "v1", + Namespace: "myns", + Resource: "jobs", + FieldSelectorRequirements: fields.Requirements{ + fields.OneTermEqualSelector("foo", "bar").Requirements()[0], + }, + }, + EnableAuthorizationSelector: true, + }, + "enabled, bad field selector": { + Verb: "GET", + Path: "/apis/batch/v1/namespaces/myns/jobs?fieldSelector=%2Abar", + ExpectedAttributes: &authorizer.AttributesRecord{ + Verb: "list", + Path: "/apis/batch/v1/namespaces/myns/jobs", + ResourceRequest: true, + APIGroup: batch.GroupName, + APIVersion: "v1", + Namespace: "myns", + Resource: "jobs", + FieldSelectorParsingErr: errors.New("invalid selector: '*bar'; can't understand '*bar'"), + }, + EnableAuthorizationSelector: true, + }, + "disabled, ignore good label selector": { + Verb: "GET", + Path: "/apis/batch/v1/namespaces/myns/jobs?labelSelector%=foo%3Dbar", + ExpectedAttributes: &authorizer.AttributesRecord{ + Verb: "list", + Path: "/apis/batch/v1/namespaces/myns/jobs", + ResourceRequest: true, + APIGroup: batch.GroupName, + APIVersion: "v1", + Namespace: "myns", + Resource: "jobs", + }, + }, + "enabled, good label selector": { + Verb: "GET", + Path: "/apis/batch/v1/namespaces/myns/jobs?labelSelector=foo%3D%3Dbar", + ExpectedAttributes: &authorizer.AttributesRecord{ + Verb: "list", + Path: "/apis/batch/v1/namespaces/myns/jobs", + ResourceRequest: true, + APIGroup: batch.GroupName, + APIVersion: "v1", + Namespace: "myns", + Resource: "jobs", + LabelSelectorRequirements: labels.Requirements{ + *basicLabelRequirement, + }, + }, + EnableAuthorizationSelector: true, + }, + "enabled, bad label selector": { + Verb: "GET", + Path: "/apis/batch/v1/namespaces/myns/jobs?labelSelector=%2Abar", + ExpectedAttributes: &authorizer.AttributesRecord{ + Verb: "list", + Path: "/apis/batch/v1/namespaces/myns/jobs", + ResourceRequest: true, + APIGroup: batch.GroupName, + APIVersion: "v1", + Namespace: "myns", + Resource: "jobs", + LabelSelectorParsingErr: errors.New("unable to parse requirement: : Invalid value: \"*bar\": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')"), + }, + EnableAuthorizationSelector: true, + }, } for k, tc := range testcases { - req, _ := http.NewRequest(tc.Verb, tc.Path, nil) - req.RemoteAddr = "127.0.0.1" + t.Run(k, func(t *testing.T) { + if tc.EnableAuthorizationSelector { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true) + } - var attribs authorizer.Attributes - var err error - var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - ctx := req.Context() - attribs, err = GetAuthorizerAttributes(ctx) + req, _ := http.NewRequest(tc.Verb, tc.Path, nil) + req.RemoteAddr = "127.0.0.1" + + var attribs authorizer.Attributes + var err error + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + attribs, err = GetAuthorizerAttributes(ctx) + }) + handler = WithRequestInfo(handler, newTestRequestInfoResolver()) + handler.ServeHTTP(httptest.NewRecorder(), req) + + if err != nil { + t.Errorf("%s: unexpected error: %v", k, err) + } else if !reflect.DeepEqual(attribs, tc.ExpectedAttributes) { + t.Errorf("%s: expected\n\t%#v\ngot\n\t%#v", k, tc.ExpectedAttributes, attribs) + } }) - handler = WithRequestInfo(handler, newTestRequestInfoResolver()) - handler.ServeHTTP(httptest.NewRecorder(), req) - - if err != nil { - t.Errorf("%s: unexpected error: %v", k, err) - } else if !reflect.DeepEqual(attribs, tc.ExpectedAttributes) { - t.Errorf("%s: expected\n\t%#v\ngot\n\t%#v", k, tc.ExpectedAttributes, attribs) - } } } diff --git a/pkg/endpoints/request/requestinfo.go b/pkg/endpoints/request/requestinfo.go index 2558494bd..808943d16 100644 --- a/pkg/endpoints/request/requestinfo.go +++ b/pkg/endpoints/request/requestinfo.go @@ -27,6 +27,8 @@ import ( metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" + genericfeatures "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/klog/v2" ) @@ -62,6 +64,13 @@ type RequestInfo struct { Name string // Parts are the path parts for the request, always starting with /{resource}/{name} Parts []string + + // FieldSelector contains the unparsed field selector from a request. It is only present if the apiserver + // honors field selectors for the verb this request is associated with. + FieldSelector string + // LabelSelector contains the unparsed field selector from a request. It is only present if the apiserver + // honors field selectors for the verb this request is associated with. + LabelSelector string } // specialVerbs contains just strings which are used in REST paths for special actions that don't fall under the normal @@ -77,6 +86,9 @@ var specialVerbsNoSubresources = sets.NewString("proxy") // this list allows the parser to distinguish between a namespace subresource, and a namespaced resource var namespaceSubresources = sets.NewString("status", "finalize") +// verbsWithSelectors is the list of verbs which support fieldSelector and labelSelector parameters +var verbsWithSelectors = sets.NewString("list", "watch", "deletecollection") + // NamespaceSubResourcesForTest exports namespaceSubresources for testing in pkg/controlplane/master_test.go, so we never drift var NamespaceSubResourcesForTest = sets.NewString(namespaceSubresources.List()...) @@ -151,6 +163,7 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er currentParts = currentParts[1:] // handle input of form /{specialVerb}/* + verbViaPathPrefix := false if specialVerbs.Has(currentParts[0]) { if len(currentParts) < 2 { return &requestInfo, fmt.Errorf("unable to determine kind and namespace from url, %v", req.URL) @@ -158,6 +171,7 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er requestInfo.Verb = currentParts[0] currentParts = currentParts[1:] + verbViaPathPrefix = true } else { switch req.Method { @@ -238,11 +252,28 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er } } } + // if there's no name on the request and we thought it was a delete before, then the actual verb is deletecollection if len(requestInfo.Name) == 0 && requestInfo.Verb == "delete" { requestInfo.Verb = "deletecollection" } + if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) { + // Don't support selector authorization on requests that used the deprecated verb-via-path mechanism, since they don't support selectors consistently. + // There are multi-object and single-object watch endpoints, and only the multi-object one supports selectors. + if !verbViaPathPrefix && verbsWithSelectors.Has(requestInfo.Verb) { + // interestingly these are parsed above, but the current structure there means that if one (or anything) in the + // listOptions fails to decode, the field and label selectors are lost. + // therefore, do the straight query param read here. + if vals := req.URL.Query()["fieldSelector"]; len(vals) > 0 { + requestInfo.FieldSelector = vals[0] + } + if vals := req.URL.Query()["labelSelector"]; len(vals) > 0 { + requestInfo.LabelSelector = vals[0] + } + } + } + return &requestInfo, nil } diff --git a/pkg/endpoints/request/requestinfo_test.go b/pkg/endpoints/request/requestinfo_test.go index a5c521e5b..552957089 100644 --- a/pkg/endpoints/request/requestinfo_test.go +++ b/pkg/endpoints/request/requestinfo_test.go @@ -25,6 +25,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" + genericfeatures "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + featuregatetesting "k8s.io/component-base/featuregate/testing" ) func TestGetAPIRequestInfo(t *testing.T) { @@ -190,64 +193,129 @@ func newTestRequestInfoResolver() *RequestInfoFactory { } } -func TestFieldSelectorParsing(t *testing.T) { +func TestSelectorParsing(t *testing.T) { tests := []struct { - name string - url string - expectedName string - expectedErr error - expectedVerb string + name string + method string + url string + expectedName string + expectedErr error + expectedVerb string + expectedFieldSelector string + expectedLabelSelector string }{ { - name: "no selector", - url: "/apis/group/version/resource", - expectedVerb: "list", + name: "no selector", + method: "GET", + url: "/apis/group/version/resource", + expectedVerb: "list", + expectedFieldSelector: "", }, { - name: "metadata.name selector", - url: "/apis/group/version/resource?fieldSelector=metadata.name=name1", - expectedName: "name1", - expectedVerb: "list", + name: "metadata.name selector", + method: "GET", + url: "/apis/group/version/resource?fieldSelector=metadata.name=name1", + expectedName: "name1", + expectedVerb: "list", + expectedFieldSelector: "metadata.name=name1", }, { - name: "metadata.name selector with watch", - url: "/apis/group/version/resource?watch=true&fieldSelector=metadata.name=name1", - expectedName: "name1", - expectedVerb: "watch", + name: "metadata.name selector with watch", + method: "GET", + url: "/apis/group/version/resource?watch=true&fieldSelector=metadata.name=name1", + expectedName: "name1", + expectedVerb: "watch", + expectedFieldSelector: "metadata.name=name1", }, { - name: "random selector", - url: "/apis/group/version/resource?fieldSelector=foo=bar", - expectedName: "", - expectedVerb: "list", + name: "random selector", + method: "GET", + url: "/apis/group/version/resource?fieldSelector=foo=bar&labelSelector=baz=qux", + expectedName: "", + expectedVerb: "list", + expectedFieldSelector: "foo=bar", + expectedLabelSelector: "baz=qux", }, { - name: "invalid selector with metadata.name", - url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo", - expectedName: "", - expectedErr: fmt.Errorf("invalid selector"), - expectedVerb: "list", + name: "invalid selector with metadata.name", + method: "GET", + url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo", + expectedName: "", + expectedErr: fmt.Errorf("invalid selector"), + expectedVerb: "list", + expectedFieldSelector: "metadata.name=name1,foo", }, { - name: "invalid selector with metadata.name with watch", - url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo&watch=true", - expectedName: "", - expectedErr: fmt.Errorf("invalid selector"), - expectedVerb: "watch", + name: "invalid selector with metadata.name with watch", + method: "GET", + url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo&watch=true", + expectedName: "", + expectedErr: fmt.Errorf("invalid selector"), + expectedVerb: "watch", + expectedFieldSelector: "metadata.name=name1,foo", }, { - name: "invalid selector with metadata.name with watch false", - url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo&watch=false", - expectedName: "", - expectedErr: fmt.Errorf("invalid selector"), - expectedVerb: "list", + name: "invalid selector with metadata.name with watch false", + method: "GET", + url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo&watch=false", + expectedName: "", + expectedErr: fmt.Errorf("invalid selector"), + expectedVerb: "list", + expectedFieldSelector: "metadata.name=name1,foo", + }, + { + name: "selector on deletecollection is honored", + method: "DELETE", + url: "/apis/group/version/resource?fieldSelector=foo=bar&labelSelector=baz=qux", + expectedName: "", + expectedVerb: "deletecollection", + expectedFieldSelector: "foo=bar", + expectedLabelSelector: "baz=qux", + }, + { + name: "selector on repeated param matches parsed param", + method: "GET", + url: "/apis/group/version/resource?fieldSelector=metadata.name=foo&fieldSelector=metadata.name=bar&labelSelector=foo=bar&labelSelector=foo=baz", + expectedName: "foo", + expectedVerb: "list", + expectedFieldSelector: "metadata.name=foo", + expectedLabelSelector: "foo=bar", + }, + { + name: "selector on other verb is ignored", + method: "GET", + url: "/apis/group/version/resource/name?fieldSelector=foo=bar&labelSelector=foo=bar", + expectedName: "name", + expectedVerb: "get", + expectedFieldSelector: "", + expectedLabelSelector: "", + }, + { + name: "selector on deprecated root type watch is not parsed", + method: "GET", + url: "/apis/group/version/watch/resource?fieldSelector=metadata.name=foo&labelSelector=foo=bar", + expectedName: "", + expectedVerb: "watch", + expectedFieldSelector: "", + expectedLabelSelector: "", + }, + { + name: "selector on deprecated root item watch is not parsed", + method: "GET", + url: "/apis/group/version/watch/resource/name?fieldSelector=metadata.name=foo&labelSelector=foo=bar", + expectedName: "name", + expectedVerb: "watch", + expectedFieldSelector: "", + expectedLabelSelector: "", }, } resolver := newTestRequestInfoResolver() + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true) + for _, tc := range tests { - req, _ := http.NewRequest("GET", tc.url, nil) + req, _ := http.NewRequest(tc.method, tc.url, nil) apiRequestInfo, err := resolver.NewRequestInfo(req) if err != nil { @@ -261,5 +329,11 @@ func TestFieldSelectorParsing(t *testing.T) { if e, a := tc.expectedVerb, apiRequestInfo.Verb; e != a { t.Errorf("%s: expected verb %v, actual %v", tc.name, e, a) } + if e, a := tc.expectedFieldSelector, apiRequestInfo.FieldSelector; e != a { + t.Errorf("%s: expected fieldSelector %v, actual %v", tc.name, e, a) + } + if e, a := tc.expectedLabelSelector, apiRequestInfo.LabelSelector; e != a { + t.Errorf("%s: expected labelSelector %v, actual %v", tc.name, e, a) + } } } diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index 200838247..6b09e9b77 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -95,6 +95,13 @@ const ( // Enables serving watch requests in separate goroutines. APIServingWithRoutine featuregate.Feature = "APIServingWithRoutine" + // owner: @deads2k + // kep: https://kep.k8s.io/4601 + // alpha: v1.31 + // + // Allows authorization to use field and label selectors. + AuthorizeWithSelectors featuregate.Feature = "AuthorizeWithSelectors" + // owner: @cici37 @jpbetz // kep: http://kep.k8s.io/3488 // alpha: v1.26 @@ -358,6 +365,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS APIServingWithRoutine: {Default: true, PreRelease: featuregate.Beta}, + AuthorizeWithSelectors: {Default: false, PreRelease: featuregate.Alpha}, + ValidatingAdmissionPolicy: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.32 CustomResourceValidationExpressions: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.31 diff --git a/plugin/pkg/authorizer/webhook/round_trip_test.go b/plugin/pkg/authorizer/webhook/round_trip_test.go index 7b5ea4cf2..73f6adf00 100644 --- a/plugin/pkg/authorizer/webhook/round_trip_test.go +++ b/plugin/pkg/authorizer/webhook/round_trip_test.go @@ -81,13 +81,15 @@ func v1beta1ResourceAttributesToV1ResourceAttributes(in *authorizationv1beta1.Re return nil } return &authorizationv1.ResourceAttributes{ - Namespace: in.Namespace, - Verb: in.Verb, - Group: in.Group, - Version: in.Version, - Resource: in.Resource, - Subresource: in.Subresource, - Name: in.Name, + Namespace: in.Namespace, + Verb: in.Verb, + Group: in.Group, + Version: in.Version, + Resource: in.Resource, + Subresource: in.Subresource, + Name: in.Name, + FieldSelector: in.FieldSelector, + LabelSelector: in.LabelSelector, } } diff --git a/plugin/pkg/authorizer/webhook/webhook.go b/plugin/pkg/authorizer/webhook/webhook.go index 589899d72..ebc4949d9 100644 --- a/plugin/pkg/authorizer/webhook/webhook.go +++ b/plugin/pkg/authorizer/webhook/webhook.go @@ -32,6 +32,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/selection" "k8s.io/apimachinery/pkg/util/cache" utilnet "k8s.io/apimachinery/pkg/util/net" "k8s.io/apimachinery/pkg/util/wait" @@ -40,7 +41,7 @@ import ( "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authorization/authorizer" authorizationcel "k8s.io/apiserver/pkg/authorization/cel" - "k8s.io/apiserver/pkg/features" + genericfeatures "k8s.io/apiserver/pkg/features" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/apiserver/pkg/util/webhook" "k8s.io/apiserver/plugin/pkg/authorizer/webhook/metrics" @@ -196,15 +197,7 @@ func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attri } if attr.IsResourceRequest() { - r.Spec.ResourceAttributes = &authorizationv1.ResourceAttributes{ - Namespace: attr.GetNamespace(), - Verb: attr.GetVerb(), - Group: attr.GetAPIGroup(), - Version: attr.GetAPIVersion(), - Resource: attr.GetResource(), - Subresource: attr.GetSubresource(), - Name: attr.GetName(), - } + r.Spec.ResourceAttributes = resourceAttributesFrom(attr) } else { r.Spec.NonResourceAttributes = &authorizationv1.NonResourceAttributes{ Path: attr.GetPath(), @@ -212,7 +205,7 @@ func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attri } } // skipping match when feature is not enabled - if utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthorizationConfiguration) { + if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StructuredAuthorizationConfiguration) { // Process Match Conditions before calling the webhook matches, err := w.match(ctx, r) // If at least one matchCondition evaluates to an error (but none are FALSE): @@ -305,6 +298,109 @@ func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attri } +func resourceAttributesFrom(attr authorizer.Attributes) *authorizationv1.ResourceAttributes { + ret := &authorizationv1.ResourceAttributes{ + Namespace: attr.GetNamespace(), + Verb: attr.GetVerb(), + Group: attr.GetAPIGroup(), + Version: attr.GetAPIVersion(), + Resource: attr.GetResource(), + Subresource: attr.GetSubresource(), + Name: attr.GetName(), + } + + if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) { + // If we are able to get any requirements while parsing selectors, use them, even if there's an error. + // This is because selectors only narrow, so if a subset of selector requirements are available, the request can be allowed. + if selectorRequirements, _ := fieldSelectorToAuthorizationAPI(attr); len(selectorRequirements) > 0 { + ret.FieldSelector = &authorizationv1.FieldSelectorAttributes{ + Requirements: selectorRequirements, + } + } + + if selectorRequirements, _ := labelSelectorToAuthorizationAPI(attr); len(selectorRequirements) > 0 { + ret.LabelSelector = &authorizationv1.LabelSelectorAttributes{ + Requirements: selectorRequirements, + } + } + } + + return ret +} + +func fieldSelectorToAuthorizationAPI(attr authorizer.Attributes) ([]metav1.FieldSelectorRequirement, error) { + requirements, getFieldSelectorErr := attr.GetFieldSelector() + if len(requirements) == 0 { + return nil, getFieldSelectorErr + } + + retRequirements := []metav1.FieldSelectorRequirement{} + for _, requirement := range requirements { + retRequirement := metav1.FieldSelectorRequirement{} + switch { + case requirement.Operator == selection.Equals || requirement.Operator == selection.DoubleEquals || requirement.Operator == selection.In: + retRequirement.Operator = metav1.FieldSelectorOpIn + retRequirement.Key = requirement.Field + retRequirement.Values = []string{requirement.Value} + case requirement.Operator == selection.NotEquals || requirement.Operator == selection.NotIn: + retRequirement.Operator = metav1.FieldSelectorOpNotIn + retRequirement.Key = requirement.Field + retRequirement.Values = []string{requirement.Value} + default: + // ignore this particular requirement. since requirements are AND'd, it is safe to ignore unknown requirements + // for authorization since the resulting check will only be as broad or broader than the intended. + continue + } + retRequirements = append(retRequirements, retRequirement) + } + + if len(retRequirements) == 0 { + // this means that all requirements were dropped (likely due to unknown operators), so we are checking the broader + // unrestricted action. + return nil, getFieldSelectorErr + } + return retRequirements, getFieldSelectorErr +} + +func labelSelectorToAuthorizationAPI(attr authorizer.Attributes) ([]metav1.LabelSelectorRequirement, error) { + requirements, getLabelSelectorErr := attr.GetLabelSelector() + if len(requirements) == 0 { + return nil, getLabelSelectorErr + } + + retRequirements := []metav1.LabelSelectorRequirement{} + for _, requirement := range requirements { + retRequirement := metav1.LabelSelectorRequirement{ + Key: requirement.Key(), + } + if values := requirement.ValuesUnsorted(); len(values) > 0 { + retRequirement.Values = values + } + switch requirement.Operator() { + case selection.Equals, selection.DoubleEquals, selection.In: + retRequirement.Operator = metav1.LabelSelectorOpIn + case selection.NotEquals, selection.NotIn: + retRequirement.Operator = metav1.LabelSelectorOpNotIn + case selection.Exists: + retRequirement.Operator = metav1.LabelSelectorOpExists + case selection.DoesNotExist: + retRequirement.Operator = metav1.LabelSelectorOpDoesNotExist + default: + // ignore this particular requirement. since requirements are AND'd, it is safe to ignore unknown requirements + // for authorization since the resulting check will only be as broad or broader than the intended. + continue + } + retRequirements = append(retRequirements, retRequirement) + } + + if len(retRequirements) == 0 { + // this means that all requirements were dropped (likely due to unknown operators), so we are checking the broader + // unrestricted action. + return nil, getLabelSelectorErr + } + return retRequirements, getLabelSelectorErr +} + // TODO: need to finish the method to get the rules when using webhook mode func (w *WebhookAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) { var ( @@ -475,13 +571,15 @@ func v1ResourceAttributesToV1beta1ResourceAttributes(in *authorizationv1.Resourc return nil } return &authorizationv1beta1.ResourceAttributes{ - Namespace: in.Namespace, - Verb: in.Verb, - Group: in.Group, - Version: in.Version, - Resource: in.Resource, - Subresource: in.Subresource, - Name: in.Name, + Namespace: in.Namespace, + Verb: in.Verb, + Group: in.Group, + Version: in.Version, + Resource: in.Resource, + Subresource: in.Subresource, + Name: in.Name, + FieldSelector: in.FieldSelector, + LabelSelector: in.LabelSelector, } } diff --git a/plugin/pkg/authorizer/webhook/webhook_test.go b/plugin/pkg/authorizer/webhook/webhook_test.go new file mode 100644 index 000000000..98c8bbacf --- /dev/null +++ b/plugin/pkg/authorizer/webhook/webhook_test.go @@ -0,0 +1,334 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webhook + +import ( + "errors" + "reflect" + "testing" + + authorizationv1 "k8s.io/api/authorization/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apiserver/pkg/authorization/authorizer" + genericfeatures "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + featuregatetesting "k8s.io/component-base/featuregate/testing" +) + +func mustLabelRequirement(selector string) labels.Requirements { + ret, err := labels.Parse(selector) + if err != nil { + panic(err) + } + requirements, _ := ret.Requirements() + return requirements +} + +func Test_resourceAttributesFrom(t *testing.T) { + type args struct { + attr authorizer.Attributes + } + tests := []struct { + name string + args args + want *authorizationv1.ResourceAttributes + enableAuthorizationSelector bool + }{ + { + name: "field selector: don't parse when disabled", + args: args{ + attr: authorizer.AttributesRecord{ + FieldSelectorRequirements: fields.Requirements{ + fields.OneTermEqualSelector("foo", "bar").Requirements()[0], + }, + FieldSelectorParsingErr: nil, + }, + }, + want: &authorizationv1.ResourceAttributes{}, + }, + { + name: "label selector: don't parse when disabled", + args: args{ + attr: authorizer.AttributesRecord{ + LabelSelectorRequirements: mustLabelRequirement("foo in (bar,baz)"), + LabelSelectorParsingErr: nil, + }, + }, + want: &authorizationv1.ResourceAttributes{}, + }, + { + name: "field selector: ignore error", + args: args{ + attr: authorizer.AttributesRecord{ + FieldSelectorRequirements: fields.Requirements{ + fields.OneTermEqualSelector("foo", "bar").Requirements()[0], + }, + FieldSelectorParsingErr: errors.New("failed"), + }, + }, + want: &authorizationv1.ResourceAttributes{ + FieldSelector: &authorizationv1.FieldSelectorAttributes{ + Requirements: []metav1.FieldSelectorRequirement{{Key: "foo", Operator: "In", Values: []string{"bar"}}}, + }, + }, + enableAuthorizationSelector: true, + }, + { + name: "label selector: ignore error", + args: args{ + attr: authorizer.AttributesRecord{ + LabelSelectorRequirements: mustLabelRequirement("foo in (bar,baz)"), + LabelSelectorParsingErr: errors.New("failed"), + }, + }, + want: &authorizationv1.ResourceAttributes{ + LabelSelector: &authorizationv1.LabelSelectorAttributes{ + Requirements: []metav1.LabelSelectorRequirement{{Key: "foo", Operator: "In", Values: []string{"bar", "baz"}}}, + }, + }, + enableAuthorizationSelector: true, + }, + { + name: "field selector: equals, double equals, in", + args: args{ + attr: authorizer.AttributesRecord{ + FieldSelectorRequirements: fields.Requirements{ + {Operator: selection.Equals, Field: "foo", Value: "bar"}, + {Operator: selection.DoubleEquals, Field: "one", Value: "two"}, + {Operator: selection.In, Field: "apple", Value: "banana"}, + }, + FieldSelectorParsingErr: nil, + }, + }, + want: &authorizationv1.ResourceAttributes{ + FieldSelector: &authorizationv1.FieldSelectorAttributes{ + Requirements: []metav1.FieldSelectorRequirement{ + { + Key: "foo", + Operator: "In", + Values: []string{"bar"}, + }, + { + Key: "one", + Operator: "In", + Values: []string{"two"}, + }, + { + Key: "apple", + Operator: "In", + Values: []string{"banana"}, + }, + }, + }, + }, + enableAuthorizationSelector: true, + }, + { + name: "field selector: not equals, not in", + args: args{ + attr: authorizer.AttributesRecord{ + FieldSelectorRequirements: fields.Requirements{ + {Operator: selection.NotEquals, Field: "foo", Value: "bar"}, + {Operator: selection.NotIn, Field: "apple", Value: "banana"}, + }, + FieldSelectorParsingErr: nil, + }, + }, + want: &authorizationv1.ResourceAttributes{ + FieldSelector: &authorizationv1.FieldSelectorAttributes{ + Requirements: []metav1.FieldSelectorRequirement{ + { + Key: "foo", + Operator: "NotIn", + Values: []string{"bar"}, + }, + { + Key: "apple", + Operator: "NotIn", + Values: []string{"banana"}, + }, + }, + }, + }, + enableAuthorizationSelector: true, + }, + { + name: "field selector: unknown operator skipped", + args: args{ + attr: authorizer.AttributesRecord{ + FieldSelectorRequirements: fields.Requirements{ + {Operator: selection.NotEquals, Field: "foo", Value: "bar"}, + {Operator: selection.Operator("bad"), Field: "apple", Value: "banana"}, + }, + FieldSelectorParsingErr: nil, + }, + }, + want: &authorizationv1.ResourceAttributes{ + FieldSelector: &authorizationv1.FieldSelectorAttributes{ + Requirements: []metav1.FieldSelectorRequirement{ + { + Key: "foo", + Operator: "NotIn", + Values: []string{"bar"}, + }, + }, + }, + }, + enableAuthorizationSelector: true, + }, + { + name: "field selector: no requirements has no fieldselector", + args: args{ + attr: authorizer.AttributesRecord{ + FieldSelectorRequirements: fields.Requirements{ + {Operator: selection.Operator("bad"), Field: "apple", Value: "banana"}, + }, + FieldSelectorParsingErr: nil, + }, + }, + want: &authorizationv1.ResourceAttributes{}, + enableAuthorizationSelector: true, + }, + { + name: "label selector: in, equals, double equals", + args: args{ + attr: authorizer.AttributesRecord{ + LabelSelectorRequirements: mustLabelRequirement("foo in (bar,baz), one=two, apple==banana"), + LabelSelectorParsingErr: nil, + }, + }, + want: &authorizationv1.ResourceAttributes{ + LabelSelector: &authorizationv1.LabelSelectorAttributes{ + Requirements: []metav1.LabelSelectorRequirement{ + { + Key: "apple", + Operator: "In", + Values: []string{"banana"}, + }, + { + Key: "foo", + Operator: "In", + Values: []string{"bar", "baz"}, + }, + { + Key: "one", + Operator: "In", + Values: []string{"two"}, + }, + }, + }, + }, + enableAuthorizationSelector: true, + }, + { + name: "label selector: not in, not equals", + args: args{ + attr: authorizer.AttributesRecord{ + LabelSelectorRequirements: mustLabelRequirement("foo notin (bar,baz), one!=two"), + LabelSelectorParsingErr: nil, + }, + }, + want: &authorizationv1.ResourceAttributes{ + LabelSelector: &authorizationv1.LabelSelectorAttributes{ + Requirements: []metav1.LabelSelectorRequirement{ + { + Key: "foo", + Operator: "NotIn", + Values: []string{"bar", "baz"}, + }, + { + Key: "one", + Operator: "NotIn", + Values: []string{"two"}, + }, + }, + }, + }, + enableAuthorizationSelector: true, + }, + { + name: "label selector: exists, not exists", + args: args{ + attr: authorizer.AttributesRecord{ + LabelSelectorRequirements: mustLabelRequirement("foo, !one"), + LabelSelectorParsingErr: nil, + }, + }, + want: &authorizationv1.ResourceAttributes{ + LabelSelector: &authorizationv1.LabelSelectorAttributes{ + Requirements: []metav1.LabelSelectorRequirement{ + { + Key: "foo", + Operator: "Exists", + }, + { + Key: "one", + Operator: "DoesNotExist", + }, + }, + }, + }, + enableAuthorizationSelector: true, + }, + { + name: "label selector: unknown operator skipped", + args: args{ + attr: authorizer.AttributesRecord{ + LabelSelectorRequirements: mustLabelRequirement("foo != bar, apple > 1"), + LabelSelectorParsingErr: nil, + }, + }, + want: &authorizationv1.ResourceAttributes{ + LabelSelector: &authorizationv1.LabelSelectorAttributes{ + Requirements: []metav1.LabelSelectorRequirement{ + { + Key: "foo", + Operator: "NotIn", + Values: []string{"bar"}, + }, + }, + }, + }, + enableAuthorizationSelector: true, + }, + { + name: "label selector: no requirements has no labelselector", + args: args{ + attr: authorizer.AttributesRecord{ + LabelSelectorRequirements: mustLabelRequirement("apple > 1"), + LabelSelectorParsingErr: nil, + }, + }, + want: &authorizationv1.ResourceAttributes{}, + enableAuthorizationSelector: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.enableAuthorizationSelector { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true) + } + + if got := resourceAttributesFrom(tt.args.attr); !reflect.DeepEqual(got, tt.want) { + t.Errorf("resourceAttributesFrom() = %v, want %v", got, tt.want) + } + }) + } +}