Add CEL fieldSelector / labelSelector support to authorizer library
This commit is contained in:
		 David Eads
					David Eads
				
			
				
					committed by
					
						 Jordan Liggitt
						Jordan Liggitt
					
				
			
			
				
	
			
			
			 Jordan Liggitt
						Jordan Liggitt
					
				
			
						parent
						
							03d48b7683
						
					
				
				
					commit
					be2e32fa3e
				
			| @@ -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 | ||||
| 		} | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user