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" | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"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" | 	"reflect" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
| @@ -125,6 +131,11 @@ func TestCompile(t *testing.T) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func TestFilter(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{ | 	configMapParams := &corev1.ConfigMap{ | ||||||
| 		ObjectMeta: metav1.ObjectMeta{ | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
| 			Name: "foo", | 			Name: "foo", | ||||||
| @@ -183,6 +194,7 @@ func TestFilter(t *testing.T) { | |||||||
| 		testPerCallLimit uint64 | 		testPerCallLimit uint64 | ||||||
| 		namespaceObject  *corev1.Namespace | 		namespaceObject  *corev1.Namespace | ||||||
| 		strictCost       bool | 		strictCost       bool | ||||||
|  | 		enableSelectors  bool | ||||||
| 	}{ | 	}{ | ||||||
| 		{ | 		{ | ||||||
| 			name: "valid syntax for object", | 			name: "valid syntax for object", | ||||||
| @@ -486,7 +498,65 @@ func TestFilter(t *testing.T) { | |||||||
| 			name: "test authorizer allow resource check with all fields", | 			name: "test authorizer allow resource check with all fields", | ||||||
| 			validations: []ExpressionAccessor{ | 			validations: []ExpressionAccessor{ | ||||||
| 				&condition{ | 				&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), | 			attributes: newValidAttribute(&podObject, false), | ||||||
| @@ -760,6 +830,10 @@ func TestFilter(t *testing.T) { | |||||||
|  |  | ||||||
| 	for _, tc := range cases { | 	for _, tc := range cases { | ||||||
| 		t.Run(tc.name, func(t *testing.T) { | 		t.Run(tc.name, func(t *testing.T) { | ||||||
|  | 			if tc.enableSelectors { | ||||||
|  | 				featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true) | ||||||
|  | 			} | ||||||
|  |  | ||||||
| 			if tc.testPerCallLimit == 0 { | 			if tc.testPerCallLimit == 0 { | ||||||
| 				tc.testPerCallLimit = celconfig.PerCallLimit | 				tc.testPerCallLimit = celconfig.PerCallLimit | ||||||
| 			} | 			} | ||||||
| @@ -1400,6 +1474,7 @@ func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) | |||||||
| 		if !ok { | 		if !ok { | ||||||
| 			panic(fmt.Sprintf("unsupported type: %T", a)) | 			panic(fmt.Sprintf("unsupported type: %T", a)) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if reflect.DeepEqual(f.match.match, *other) { | 		if reflect.DeepEqual(f.match.match, *other) { | ||||||
| 			return f.match.decision, f.match.reason, f.match.err | 			return f.match.decision, f.match.reason, f.match.err | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -19,6 +19,10 @@ package library | |||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"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" | 	"reflect" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| @@ -222,6 +226,12 @@ var authzLibraryDecls = map[string][]cel.FunctionOpt{ | |||||||
| 	"subresource": { | 	"subresource": { | ||||||
| 		cel.MemberOverload("resourcecheck_subresource", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType, | 		cel.MemberOverload("resourcecheck_subresource", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType, | ||||||
| 			cel.BinaryBinding(resourceCheckSubresource))}, | 			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": { | 	"namespace": { | ||||||
| 		cel.MemberOverload("resourcecheck_namespace", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType, | 		cel.MemberOverload("resourcecheck_namespace", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType, | ||||||
| 			cel.BinaryBinding(resourceCheckNamespace))}, | 			cel.BinaryBinding(resourceCheckNamespace))}, | ||||||
| @@ -354,6 +364,38 @@ func resourceCheckSubresource(arg1, arg2 ref.Val) ref.Val { | |||||||
| 	return result | 	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 { | func resourceCheckNamespace(arg1, arg2 ref.Val) ref.Val { | ||||||
| 	resourceCheck, ok := arg1.(resourceCheckVal) | 	resourceCheck, ok := arg1.(resourceCheckVal) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| @@ -549,6 +591,8 @@ type resourceCheckVal struct { | |||||||
| 	subresource   string | 	subresource   string | ||||||
| 	namespace     string | 	namespace     string | ||||||
| 	name          string | 	name          string | ||||||
|  | 	fieldSelector string | ||||||
|  | 	labelSelector string | ||||||
| } | } | ||||||
|  |  | ||||||
| func (a resourceCheckVal) Authorize(ctx context.Context, verb string) ref.Val { | 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, | 		Verb:            verb, | ||||||
| 		User:            a.groupCheck.authorizer.userInfo, | 		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) | 	decision, reason, err := a.groupCheck.authorizer.authAuthorizer.Authorize(ctx, attr) | ||||||
| 	return newDecision(decision, err, reason) | 	return newDecision(decision, err, reason) | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user