diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter_test.go index 8ce7d470955..a87b24474a0 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter_test.go @@ -20,6 +20,12 @@ import ( "context" "errors" "fmt" + "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" "reflect" "strings" "testing" @@ -125,6 +131,11 @@ func TestCompile(t *testing.T) { } func TestFilter(t *testing.T) { + simpleLabelSelector, err := labels.NewRequirement("apple", selection.Equals, []string{"banana"}) + if err != nil { + panic(err) + } + configMapParams := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", @@ -183,6 +194,7 @@ func TestFilter(t *testing.T) { testPerCallLimit uint64 namespaceObject *corev1.Namespace strictCost bool + enableSelectors bool }{ { name: "valid syntax for object", @@ -486,7 +498,65 @@ func TestFilter(t *testing.T) { name: "test authorizer allow resource check with all fields", validations: []ExpressionAccessor{ &condition{ - Expression: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()", + Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo=bar').labelSelector('apple=banana').subresource('status').namespace('test').name('backend').check('create').allowed()", + }, + }, + attributes: newValidAttribute(&podObject, false), + results: []EvaluationResult{ + { + EvalResult: celtypes.True, + }, + }, + authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{ + ResourceRequest: true, + APIGroup: "apps", + Resource: "deployments", + Subresource: "status", + Namespace: "test", + Name: "backend", + Verb: "create", + APIVersion: "*", + FieldSelectorRequirements: fields.Requirements{ + {Operator: "=", Field: "foo", Value: "bar"}, + }, + LabelSelectorRequirements: labels.Requirements{ + *simpleLabelSelector, + }, + }), + enableSelectors: true, + }, + { + name: "test authorizer allow resource check with parse failures", + validations: []ExpressionAccessor{ + &condition{ + Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo badoperator bar').labelSelector('apple badoperator banana').subresource('status').namespace('test').name('backend').check('create').allowed()", + }, + }, + attributes: newValidAttribute(&podObject, false), + results: []EvaluationResult{ + { + EvalResult: celtypes.True, + }, + }, + authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{ + ResourceRequest: true, + APIGroup: "apps", + Resource: "deployments", + Subresource: "status", + Namespace: "test", + Name: "backend", + Verb: "create", + APIVersion: "*", + FieldSelectorParsingErr: errors.New("invalid selector: 'foo badoperator bar'; can't understand 'foo badoperator bar'"), + LabelSelectorParsingErr: errors.New("unable to parse requirement: found 'badoperator', expected: in, notin, =, ==, !=, gt, lt"), + }), + enableSelectors: true, + }, + { + name: "test authorizer allow resource check with all fields, without gate", + validations: []ExpressionAccessor{ + &condition{ + Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo=bar').labelSelector('apple=banana').subresource('status').namespace('test').name('backend').check('create').allowed()", }, }, attributes: newValidAttribute(&podObject, false), @@ -760,6 +830,10 @@ func TestFilter(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { + if tc.enableSelectors { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true) + } + if tc.testPerCallLimit == 0 { tc.testPerCallLimit = celconfig.PerCallLimit } @@ -1400,6 +1474,7 @@ func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) if !ok { panic(fmt.Sprintf("unsupported type: %T", a)) } + if reflect.DeepEqual(f.match.match, *other) { return f.match.decision, f.match.reason, f.match.err } diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/authz.go b/staging/src/k8s.io/apiserver/pkg/cel/library/authz.go index df4bf080714..63fde879445 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/authz.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/authz.go @@ -19,6 +19,10 @@ package library import ( "context" "fmt" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + genericfeatures "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" "reflect" "strings" @@ -222,6 +226,12 @@ var authzLibraryDecls = map[string][]cel.FunctionOpt{ "subresource": { cel.MemberOverload("resourcecheck_subresource", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType, cel.BinaryBinding(resourceCheckSubresource))}, + "fieldSelector": { + cel.MemberOverload("authorizer_fieldselector", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType, + cel.BinaryBinding(resourceCheckFieldSelector))}, + "labelSelector": { + cel.MemberOverload("authorizer_labelselector", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType, + cel.BinaryBinding(resourceCheckLabelSelector))}, "namespace": { cel.MemberOverload("resourcecheck_namespace", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType, cel.BinaryBinding(resourceCheckNamespace))}, @@ -354,6 +364,38 @@ func resourceCheckSubresource(arg1, arg2 ref.Val) ref.Val { return result } +func resourceCheckFieldSelector(arg1, arg2 ref.Val) ref.Val { + resourceCheck, ok := arg1.(resourceCheckVal) + if !ok { + return types.MaybeNoSuchOverloadErr(arg1) + } + + fieldSelector, ok := arg2.Value().(string) + if !ok { + return types.MaybeNoSuchOverloadErr(arg1) + } + + result := resourceCheck + result.fieldSelector = fieldSelector + return result +} + +func resourceCheckLabelSelector(arg1, arg2 ref.Val) ref.Val { + resourceCheck, ok := arg1.(resourceCheckVal) + if !ok { + return types.MaybeNoSuchOverloadErr(arg1) + } + + labelSelector, ok := arg2.Value().(string) + if !ok { + return types.MaybeNoSuchOverloadErr(arg1) + } + + result := resourceCheck + result.labelSelector = labelSelector + return result +} + func resourceCheckNamespace(arg1, arg2 ref.Val) ref.Val { resourceCheck, ok := arg1.(resourceCheckVal) if !ok { @@ -544,11 +586,13 @@ func (g groupCheckVal) resourceCheck(resource string) resourceCheckVal { type resourceCheckVal struct { receiverOnlyObjectVal - groupCheck groupCheckVal - resource string - subresource string - namespace string - name string + groupCheck groupCheckVal + resource string + subresource string + namespace string + name string + fieldSelector string + labelSelector string } func (a resourceCheckVal) Authorize(ctx context.Context, verb string) ref.Val { @@ -563,6 +607,26 @@ func (a resourceCheckVal) Authorize(ctx context.Context, verb string) ref.Val { Verb: verb, User: a.groupCheck.authorizer.userInfo, } + + if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) { + if len(a.fieldSelector) > 0 { + selector, err := fields.ParseSelector(a.fieldSelector) + if err != nil { + attr.FieldSelectorRequirements, attr.FieldSelectorParsingErr = nil, err + } else { + attr.FieldSelectorRequirements, attr.FieldSelectorParsingErr = selector.Requirements(), nil + } + } + if len(a.labelSelector) > 0 { + requirements, err := labels.ParseToRequirements(a.labelSelector) + if err != nil { + attr.LabelSelectorRequirements, attr.LabelSelectorParsingErr = nil, err + } else { + attr.LabelSelectorRequirements, attr.LabelSelectorParsingErr = requirements, nil + } + } + } + decision, reason, err := a.groupCheck.authorizer.authAuthorizer.Authorize(ctx, attr) return newDecision(decision, err, reason) }