add field and label selectors to authorization attributes

Co-authored-by: Jordan Liggitt <liggitt@google.com>

Kubernetes-commit: 92e3445e9d7a587ddb56b3ff4b1445244fbf9abd
This commit is contained in:
David Eads 2024-05-23 15:12:26 -04:00 committed by Kubernetes Publisher
parent 6dd5496a01
commit f26d4ed894
11 changed files with 1093 additions and 90 deletions

View File

@ -22,6 +22,8 @@ import (
"sort" "sort"
"strings" "strings"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
) )
@ -58,6 +60,8 @@ var _ authorizer.Attributes = (interface {
GetAPIVersion() string GetAPIVersion() string
IsResourceRequest() bool IsResourceRequest() bool
GetPath() string GetPath() string
GetFieldSelector() (fields.Requirements, error)
GetLabelSelector() (labels.Requirements, error)
})(nil) })(nil)
// The user info accessors known to cache key construction. If this fails to compile, the cache // 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 // 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. // 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) { func (ca *cachingAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
serializableAttributes := authorizer.AttributesRecord{ type SerializableAttributes struct {
Verb: a.GetVerb(), authorizer.AttributesRecord
Namespace: a.GetNamespace(), LabelSelector string
APIGroup: a.GetAPIGroup(), }
APIVersion: a.GetAPIVersion(),
Resource: a.GetResource(), serializableAttributes := SerializableAttributes{
Subresource: a.GetSubresource(), AttributesRecord: authorizer.AttributesRecord{
Name: a.GetName(), Verb: a.GetVerb(),
ResourceRequest: a.IsResourceRequest(), Namespace: a.GetNamespace(),
Path: a.GetPath(), 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 { if u := a.GetUser(); u != nil {

View File

@ -18,14 +18,31 @@ package validating
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"testing" "testing"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer" "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) { func TestCachingAuthorizer(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
type result struct { type result struct {
decision authorizer.Decision decision authorizer.Decision
reason string 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) { t.Run(tc.name, func(t *testing.T) {
var misses int var misses int

View File

@ -20,6 +20,8 @@ import (
"context" "context"
"net/http" "net/http"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
) )
@ -62,6 +64,16 @@ type Attributes interface {
// GetPath returns the path of the request // GetPath returns the path of the request
GetPath() string 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 // Authorizer makes an authorization decision based on information gained by making
@ -100,6 +112,11 @@ type AttributesRecord struct {
Name string Name string
ResourceRequest bool ResourceRequest bool
Path string Path string
FieldSelectorRequirements fields.Requirements
FieldSelectorParsingErr error
LabelSelectorRequirements labels.Requirements
LabelSelectorParsingErr error
} }
func (a AttributesRecord) GetUser() user.Info { func (a AttributesRecord) GetUser() user.Info {
@ -146,6 +163,14 @@ func (a AttributesRecord) GetPath() string {
return a.Path 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 type Decision int
const ( const (

View File

@ -22,6 +22,11 @@ import (
"net/http" "net/http"
"time" "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/klog/v2"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -117,5 +122,31 @@ func GetAuthorizerAttributes(ctx context.Context) (authorizer.Attributes, error)
attribs.Namespace = requestInfo.Namespace attribs.Namespace = requestInfo.Namespace
attribs.Name = requestInfo.Name 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 return &attribs, nil
} }

View File

@ -19,6 +19,12 @@ package filters
import ( import (
"context" "context"
"errors" "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"
"net/http/httptest" "net/http/httptest"
"reflect" "reflect"
@ -34,10 +40,16 @@ import (
) )
func TestGetAuthorizerAttributes(t *testing.T) { func TestGetAuthorizerAttributes(t *testing.T) {
basicLabelRequirement, err := labels.NewRequirement("foo", selection.DoubleEquals, []string{"bar"})
if err != nil {
t.Fatal(err)
}
testcases := map[string]struct { testcases := map[string]struct {
Verb string Verb string
Path string Path string
ExpectedAttributes *authorizer.AttributesRecord ExpectedAttributes *authorizer.AttributesRecord
EnableAuthorizationSelector bool
}{ }{
"non-resource root": { "non-resource root": {
Verb: "POST", Verb: "POST",
@ -102,26 +114,122 @@ func TestGetAuthorizerAttributes(t *testing.T) {
Resource: "jobs", 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: <nil>: 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 { for k, tc := range testcases {
req, _ := http.NewRequest(tc.Verb, tc.Path, nil) t.Run(k, func(t *testing.T) {
req.RemoteAddr = "127.0.0.1" if tc.EnableAuthorizationSelector {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
}
var attribs authorizer.Attributes req, _ := http.NewRequest(tc.Verb, tc.Path, nil)
var err error req.RemoteAddr = "127.0.0.1"
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx := req.Context() var attribs authorizer.Attributes
attribs, err = GetAuthorizerAttributes(ctx) 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)
}
} }
} }

View File

@ -27,6 +27,8 @@ import (
metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme" metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/klog/v2" "k8s.io/klog/v2"
) )
@ -62,6 +64,13 @@ type RequestInfo struct {
Name string Name string
// Parts are the path parts for the request, always starting with /{resource}/{name} // Parts are the path parts for the request, always starting with /{resource}/{name}
Parts []string 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 // 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 // this list allows the parser to distinguish between a namespace subresource, and a namespaced resource
var namespaceSubresources = sets.NewString("status", "finalize") 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 // NamespaceSubResourcesForTest exports namespaceSubresources for testing in pkg/controlplane/master_test.go, so we never drift
var NamespaceSubResourcesForTest = sets.NewString(namespaceSubresources.List()...) var NamespaceSubResourcesForTest = sets.NewString(namespaceSubresources.List()...)
@ -151,6 +163,7 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er
currentParts = currentParts[1:] currentParts = currentParts[1:]
// handle input of form /{specialVerb}/* // handle input of form /{specialVerb}/*
verbViaPathPrefix := false
if specialVerbs.Has(currentParts[0]) { if specialVerbs.Has(currentParts[0]) {
if len(currentParts) < 2 { if len(currentParts) < 2 {
return &requestInfo, fmt.Errorf("unable to determine kind and namespace from url, %v", req.URL) 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] requestInfo.Verb = currentParts[0]
currentParts = currentParts[1:] currentParts = currentParts[1:]
verbViaPathPrefix = true
} else { } else {
switch req.Method { 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 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" { if len(requestInfo.Name) == 0 && requestInfo.Verb == "delete" {
requestInfo.Verb = "deletecollection" 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 return &requestInfo, nil
} }

View File

@ -25,6 +25,9 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets" "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) { func TestGetAPIRequestInfo(t *testing.T) {
@ -190,64 +193,129 @@ func newTestRequestInfoResolver() *RequestInfoFactory {
} }
} }
func TestFieldSelectorParsing(t *testing.T) { func TestSelectorParsing(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
url string method string
expectedName string url string
expectedErr error expectedName string
expectedVerb string expectedErr error
expectedVerb string
expectedFieldSelector string
expectedLabelSelector string
}{ }{
{ {
name: "no selector", name: "no selector",
url: "/apis/group/version/resource", method: "GET",
expectedVerb: "list", url: "/apis/group/version/resource",
expectedVerb: "list",
expectedFieldSelector: "",
}, },
{ {
name: "metadata.name selector", name: "metadata.name selector",
url: "/apis/group/version/resource?fieldSelector=metadata.name=name1", method: "GET",
expectedName: "name1", url: "/apis/group/version/resource?fieldSelector=metadata.name=name1",
expectedVerb: "list", expectedName: "name1",
expectedVerb: "list",
expectedFieldSelector: "metadata.name=name1",
}, },
{ {
name: "metadata.name selector with watch", name: "metadata.name selector with watch",
url: "/apis/group/version/resource?watch=true&fieldSelector=metadata.name=name1", method: "GET",
expectedName: "name1", url: "/apis/group/version/resource?watch=true&fieldSelector=metadata.name=name1",
expectedVerb: "watch", expectedName: "name1",
expectedVerb: "watch",
expectedFieldSelector: "metadata.name=name1",
}, },
{ {
name: "random selector", name: "random selector",
url: "/apis/group/version/resource?fieldSelector=foo=bar", method: "GET",
expectedName: "", url: "/apis/group/version/resource?fieldSelector=foo=bar&labelSelector=baz=qux",
expectedVerb: "list", expectedName: "",
expectedVerb: "list",
expectedFieldSelector: "foo=bar",
expectedLabelSelector: "baz=qux",
}, },
{ {
name: "invalid selector with metadata.name", name: "invalid selector with metadata.name",
url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo", method: "GET",
expectedName: "", url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo",
expectedErr: fmt.Errorf("invalid selector"), expectedName: "",
expectedVerb: "list", expectedErr: fmt.Errorf("invalid selector"),
expectedVerb: "list",
expectedFieldSelector: "metadata.name=name1,foo",
}, },
{ {
name: "invalid selector with metadata.name with watch", name: "invalid selector with metadata.name with watch",
url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo&watch=true", method: "GET",
expectedName: "", url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo&watch=true",
expectedErr: fmt.Errorf("invalid selector"), expectedName: "",
expectedVerb: "watch", expectedErr: fmt.Errorf("invalid selector"),
expectedVerb: "watch",
expectedFieldSelector: "metadata.name=name1,foo",
}, },
{ {
name: "invalid selector with metadata.name with watch false", name: "invalid selector with metadata.name with watch false",
url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo&watch=false", method: "GET",
expectedName: "", url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo&watch=false",
expectedErr: fmt.Errorf("invalid selector"), expectedName: "",
expectedVerb: "list", 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() resolver := newTestRequestInfoResolver()
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
for _, tc := range tests { for _, tc := range tests {
req, _ := http.NewRequest("GET", tc.url, nil) req, _ := http.NewRequest(tc.method, tc.url, nil)
apiRequestInfo, err := resolver.NewRequestInfo(req) apiRequestInfo, err := resolver.NewRequestInfo(req)
if err != nil { if err != nil {
@ -261,5 +329,11 @@ func TestFieldSelectorParsing(t *testing.T) {
if e, a := tc.expectedVerb, apiRequestInfo.Verb; e != a { if e, a := tc.expectedVerb, apiRequestInfo.Verb; e != a {
t.Errorf("%s: expected verb %v, actual %v", tc.name, 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)
}
} }
} }

View File

@ -95,6 +95,13 @@ const (
// Enables serving watch requests in separate goroutines. // Enables serving watch requests in separate goroutines.
APIServingWithRoutine featuregate.Feature = "APIServingWithRoutine" 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 // owner: @cici37 @jpbetz
// kep: http://kep.k8s.io/3488 // kep: http://kep.k8s.io/3488
// alpha: v1.26 // alpha: v1.26
@ -358,6 +365,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
APIServingWithRoutine: {Default: true, PreRelease: featuregate.Beta}, APIServingWithRoutine: {Default: true, PreRelease: featuregate.Beta},
AuthorizeWithSelectors: {Default: false, PreRelease: featuregate.Alpha},
ValidatingAdmissionPolicy: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.32 ValidatingAdmissionPolicy: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.32
CustomResourceValidationExpressions: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.31 CustomResourceValidationExpressions: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.31

View File

@ -81,13 +81,15 @@ func v1beta1ResourceAttributesToV1ResourceAttributes(in *authorizationv1beta1.Re
return nil return nil
} }
return &authorizationv1.ResourceAttributes{ return &authorizationv1.ResourceAttributes{
Namespace: in.Namespace, Namespace: in.Namespace,
Verb: in.Verb, Verb: in.Verb,
Group: in.Group, Group: in.Group,
Version: in.Version, Version: in.Version,
Resource: in.Resource, Resource: in.Resource,
Subresource: in.Subresource, Subresource: in.Subresource,
Name: in.Name, Name: in.Name,
FieldSelector: in.FieldSelector,
LabelSelector: in.LabelSelector,
} }
} }

View File

@ -32,6 +32,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/util/cache" "k8s.io/apimachinery/pkg/util/cache"
utilnet "k8s.io/apimachinery/pkg/util/net" utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
@ -40,7 +41,7 @@ import (
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
authorizationcel "k8s.io/apiserver/pkg/authorization/cel" 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" utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/apiserver/pkg/util/webhook" "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/apiserver/plugin/pkg/authorizer/webhook/metrics" "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() { if attr.IsResourceRequest() {
r.Spec.ResourceAttributes = &authorizationv1.ResourceAttributes{ r.Spec.ResourceAttributes = resourceAttributesFrom(attr)
Namespace: attr.GetNamespace(),
Verb: attr.GetVerb(),
Group: attr.GetAPIGroup(),
Version: attr.GetAPIVersion(),
Resource: attr.GetResource(),
Subresource: attr.GetSubresource(),
Name: attr.GetName(),
}
} else { } else {
r.Spec.NonResourceAttributes = &authorizationv1.NonResourceAttributes{ r.Spec.NonResourceAttributes = &authorizationv1.NonResourceAttributes{
Path: attr.GetPath(), Path: attr.GetPath(),
@ -212,7 +205,7 @@ func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attri
} }
} }
// skipping match when feature is not enabled // 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 // Process Match Conditions before calling the webhook
matches, err := w.match(ctx, r) matches, err := w.match(ctx, r)
// If at least one matchCondition evaluates to an error (but none are FALSE): // 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 // 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) { func (w *WebhookAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
var ( var (
@ -475,13 +571,15 @@ func v1ResourceAttributesToV1beta1ResourceAttributes(in *authorizationv1.Resourc
return nil return nil
} }
return &authorizationv1beta1.ResourceAttributes{ return &authorizationv1beta1.ResourceAttributes{
Namespace: in.Namespace, Namespace: in.Namespace,
Verb: in.Verb, Verb: in.Verb,
Group: in.Group, Group: in.Group,
Version: in.Version, Version: in.Version,
Resource: in.Resource, Resource: in.Resource,
Subresource: in.Subresource, Subresource: in.Subresource,
Name: in.Name, Name: in.Name,
FieldSelector: in.FieldSelector,
LabelSelector: in.LabelSelector,
} }
} }

View File

@ -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)
}
})
}
}