add field and label selectors to authorization attributes
Co-authored-by: Jordan Liggitt <liggitt@google.com>
This commit is contained in:
		 David Eads
					David Eads
				
			
				
					committed by
					
						 Jordan Liggitt
						Jordan Liggitt
					
				
			
			
				
	
			
			
			 Jordan Liggitt
						Jordan Liggitt
					
				
			
						parent
						
							f5e5bef2e0
						
					
				
				
					commit
					92e3445e9d
				
			| @@ -17,8 +17,11 @@ limitations under the License. | ||||
| package validation | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	apiequality "k8s.io/apimachinery/pkg/api/equality" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" | ||||
| 	"k8s.io/apimachinery/pkg/util/validation/field" | ||||
| 	authorizationapi "k8s.io/kubernetes/pkg/apis/authorization" | ||||
| ) | ||||
| @@ -36,6 +39,7 @@ func ValidateSubjectAccessReviewSpec(spec authorizationapi.SubjectAccessReviewSp | ||||
| 	if len(spec.User) == 0 && len(spec.Groups) == 0 { | ||||
| 		allErrs = append(allErrs, field.Invalid(fldPath.Child("user"), spec.User, `at least one of user or group must be specified`)) | ||||
| 	} | ||||
| 	allErrs = append(allErrs, validateResourceAttributes(spec.ResourceAttributes, field.NewPath("spec.resourceAttributes"))...) | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
| @@ -50,6 +54,7 @@ func ValidateSelfSubjectAccessReviewSpec(spec authorizationapi.SelfSubjectAccess | ||||
| 	if spec.ResourceAttributes == nil && spec.NonResourceAttributes == nil { | ||||
| 		allErrs = append(allErrs, field.Invalid(fldPath.Child("resourceAttributes"), spec.NonResourceAttributes, `exactly one of nonResourceAttributes or resourceAttributes must be specified`)) | ||||
| 	} | ||||
| 	allErrs = append(allErrs, validateResourceAttributes(spec.ResourceAttributes, field.NewPath("spec.resourceAttributes"))...) | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
| @@ -99,3 +104,59 @@ func ValidateLocalSubjectAccessReview(sar *authorizationapi.LocalSubjectAccessRe | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
| func validateResourceAttributes(resourceAttributes *authorizationapi.ResourceAttributes, fldPath *field.Path) field.ErrorList { | ||||
| 	if resourceAttributes == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	allErrs := field.ErrorList{} | ||||
|  | ||||
| 	allErrs = append(allErrs, validateFieldSelectorAttributes(resourceAttributes.FieldSelector, fldPath.Child("fieldSelector"))...) | ||||
| 	allErrs = append(allErrs, validateLabelSelectorAttributes(resourceAttributes.LabelSelector, fldPath.Child("labelSelector"))...) | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
| func validateFieldSelectorAttributes(selector *authorizationapi.FieldSelectorAttributes, fldPath *field.Path) field.ErrorList { | ||||
| 	if selector == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	allErrs := field.ErrorList{} | ||||
|  | ||||
| 	if len(selector.RawSelector) > 0 && len(selector.Requirements) > 0 { | ||||
| 		allErrs = append(allErrs, field.Invalid(fldPath.Child("rawSelector"), selector.RawSelector, "may not specified at the same time as requirements")) | ||||
| 	} | ||||
| 	if len(selector.RawSelector) == 0 && len(selector.Requirements) == 0 { | ||||
| 		allErrs = append(allErrs, field.Required(fldPath.Child("requirements"), fmt.Sprintf("when %s is specified, requirements or rawSelector is required", fldPath))) | ||||
| 	} | ||||
|  | ||||
| 	// AllowUnknownOperatorInRequirement enables *SubjectAccessReview requests from newer skewed clients which understand operators kube-apiserver does not know about to be authorized. | ||||
| 	validationOptions := metav1validation.FieldSelectorValidationOptions{AllowUnknownOperatorInRequirement: true} | ||||
| 	for i, requirement := range selector.Requirements { | ||||
| 		allErrs = append(allErrs, metav1validation.ValidateFieldSelectorRequirement(requirement, validationOptions, fldPath.Child("requirements").Index(i))...) | ||||
| 	} | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
| func validateLabelSelectorAttributes(selector *authorizationapi.LabelSelectorAttributes, fldPath *field.Path) field.ErrorList { | ||||
| 	if selector == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	allErrs := field.ErrorList{} | ||||
|  | ||||
| 	if len(selector.RawSelector) > 0 && len(selector.Requirements) > 0 { | ||||
| 		allErrs = append(allErrs, field.Invalid(fldPath.Child("rawSelector"), selector.RawSelector, "may not specified at the same time as requirements")) | ||||
| 	} | ||||
| 	if len(selector.RawSelector) == 0 && len(selector.Requirements) == 0 { | ||||
| 		allErrs = append(allErrs, field.Required(fldPath.Child("requirements"), fmt.Sprintf("when %s is specified, requirements or rawSelector is required", fldPath))) | ||||
| 	} | ||||
|  | ||||
| 	// AllowUnknownOperatorInRequirement enables *SubjectAccessReview requests from newer skewed clients which understand operators kube-apiserver does not know about to be authorized. | ||||
| 	validationOptions := metav1validation.LabelSelectorValidationOptions{AllowUnknownOperatorInRequirement: true} | ||||
| 	for i, requirement := range selector.Requirements { | ||||
| 		allErrs = append(allErrs, metav1validation.ValidateLabelSelectorRequirement(requirement, validationOptions, fldPath.Child("requirements").Index(i))...) | ||||
| 	} | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
|   | ||||
| @@ -29,6 +29,50 @@ func TestValidateSARSpec(t *testing.T) { | ||||
| 	successCases := []authorizationapi.SubjectAccessReviewSpec{ | ||||
| 		{ResourceAttributes: &authorizationapi.ResourceAttributes{}, User: "me"}, | ||||
| 		{NonResourceAttributes: &authorizationapi.NonResourceAttributes{}, Groups: []string{"my-group"}}, | ||||
| 		{ // field raw selector | ||||
| 			User: "me", | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 				FieldSelector: &authorizationapi.FieldSelectorAttributes{ | ||||
| 					RawSelector: "***foo", | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ // label raw selector | ||||
| 			User: "me", | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 				LabelSelector: &authorizationapi.LabelSelectorAttributes{ | ||||
| 					RawSelector: "***foo", | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ // unknown field operator | ||||
| 			User: "me", | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 				FieldSelector: &authorizationapi.FieldSelectorAttributes{ | ||||
| 					Requirements: []metav1.FieldSelectorRequirement{ | ||||
| 						{ | ||||
| 							Key:      "k", | ||||
| 							Operator: metav1.FieldSelectorOperator("fake"), | ||||
| 							Values:   []string{"val"}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ // unknown label operator | ||||
| 			User: "me", | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 				LabelSelector: &authorizationapi.LabelSelectorAttributes{ | ||||
| 					Requirements: []metav1.LabelSelectorRequirement{ | ||||
| 						{ | ||||
| 							Key:      "k", | ||||
| 							Operator: metav1.LabelSelectorOperator("fake"), | ||||
| 							Values:   []string{"val"}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, successCase := range successCases { | ||||
| 		if errs := ValidateSubjectAccessReviewSpec(successCase, field.NewPath("spec")); len(errs) != 0 { | ||||
| @@ -58,9 +102,237 @@ func TestValidateSARSpec(t *testing.T) { | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{}, | ||||
| 		}, | ||||
| 		msg: `spec.user: Invalid value: "": at least one of user or group must be specified`, | ||||
| 	}, { | ||||
| 		name: "resource attributes: field selector specify both", | ||||
| 		obj: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 			User: "me", | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 				FieldSelector: &authorizationapi.FieldSelectorAttributes{ | ||||
| 					RawSelector: "foo", | ||||
| 					Requirements: []metav1.FieldSelectorRequirement{ | ||||
| 						{}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		msg: `spec.resourceAttributes.fieldSelector.rawSelector: Invalid value: "foo": may not specified at the same time as requirements`, | ||||
| 	}, { | ||||
| 		name: "resource attributes: field selector specify neither", | ||||
| 		obj: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 			User: "me", | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 				FieldSelector: &authorizationapi.FieldSelectorAttributes{}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		msg: `spec.resourceAttributes.fieldSelector.requirements: Required value: when spec.resourceAttributes.fieldSelector is specified, requirements or rawSelector is required`, | ||||
| 	}, { | ||||
| 		name: "resource attributes: field selector no key", | ||||
| 		obj: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 			User: "me", | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 				FieldSelector: &authorizationapi.FieldSelectorAttributes{ | ||||
| 					Requirements: []metav1.FieldSelectorRequirement{ | ||||
| 						{ | ||||
| 							Key: "", | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		msg: `spec.resourceAttributes.fieldSelector.requirements[0].key: Required value: must be specified`, | ||||
| 	}, { | ||||
| 		name: "resource attributes: field selector no value for in", | ||||
| 		obj: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 			User: "me", | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 				FieldSelector: &authorizationapi.FieldSelectorAttributes{ | ||||
| 					Requirements: []metav1.FieldSelectorRequirement{ | ||||
| 						{ | ||||
| 							Key:      "k", | ||||
| 							Operator: metav1.FieldSelectorOpIn, | ||||
| 							Values:   []string{}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		msg: "spec.resourceAttributes.fieldSelector.requirements[0].values: Required value: must be specified when `operator` is 'In' or 'NotIn'", | ||||
| 	}, { | ||||
| 		name: "resource attributes: field selector no value for not in", | ||||
| 		obj: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 			User: "me", | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 				FieldSelector: &authorizationapi.FieldSelectorAttributes{ | ||||
| 					Requirements: []metav1.FieldSelectorRequirement{ | ||||
| 						{ | ||||
| 							Key:      "k", | ||||
| 							Operator: metav1.FieldSelectorOpNotIn, | ||||
| 							Values:   []string{}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		msg: "spec.resourceAttributes.fieldSelector.requirements[0].values: Required value: must be specified when `operator` is 'In' or 'NotIn'", | ||||
| 	}, { | ||||
| 		name: "resource attributes: field selector values for exists", | ||||
| 		obj: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 			User: "me", | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 				FieldSelector: &authorizationapi.FieldSelectorAttributes{ | ||||
| 					Requirements: []metav1.FieldSelectorRequirement{ | ||||
| 						{ | ||||
| 							Key:      "k", | ||||
| 							Operator: metav1.FieldSelectorOpExists, | ||||
| 							Values:   []string{"val"}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		msg: "spec.resourceAttributes.fieldSelector.requirements[0].values: Forbidden: may not be specified when `operator` is 'Exists' or 'DoesNotExist'", | ||||
| 	}, { | ||||
| 		name: "resource attributes: field selector values for not exists", | ||||
| 		obj: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 			User: "me", | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 				FieldSelector: &authorizationapi.FieldSelectorAttributes{ | ||||
| 					Requirements: []metav1.FieldSelectorRequirement{ | ||||
| 						{ | ||||
| 							Key:      "k", | ||||
| 							Operator: metav1.FieldSelectorOpDoesNotExist, | ||||
| 							Values:   []string{"val"}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		msg: "spec.resourceAttributes.fieldSelector.requirements[0].values: Forbidden: may not be specified when `operator` is 'Exists' or 'DoesNotExist'", | ||||
| 	}, { | ||||
| 		name: "resource attributes: label selector specify both", | ||||
| 		obj: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 			User: "me", | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 				LabelSelector: &authorizationapi.LabelSelectorAttributes{ | ||||
| 					RawSelector: "foo", | ||||
| 					Requirements: []metav1.LabelSelectorRequirement{ | ||||
| 						{}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		msg: `spec.resourceAttributes.labelSelector.rawSelector: Invalid value: "foo": may not specified at the same time as requirements`, | ||||
| 	}, { | ||||
| 		name: "resource attributes: label selector specify neither", | ||||
| 		obj: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 			User: "me", | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 				LabelSelector: &authorizationapi.LabelSelectorAttributes{}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		msg: `spec.resourceAttributes.labelSelector.requirements: Required value: when spec.resourceAttributes.labelSelector is specified, requirements or rawSelector is required`, | ||||
| 	}, { | ||||
| 		name: "resource attributes: label selector no key", | ||||
| 		obj: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 			User: "me", | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 				LabelSelector: &authorizationapi.LabelSelectorAttributes{ | ||||
| 					Requirements: []metav1.LabelSelectorRequirement{ | ||||
| 						{ | ||||
| 							Key: "", | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		msg: `spec.resourceAttributes.labelSelector.requirements[0].key: Invalid value: "": name part must be non-empty`, | ||||
| 	}, { | ||||
| 		name: "resource attributes: label selector invalid label name", | ||||
| 		obj: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 			User: "me", | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 				LabelSelector: &authorizationapi.LabelSelectorAttributes{ | ||||
| 					Requirements: []metav1.LabelSelectorRequirement{ | ||||
| 						{ | ||||
| 							Key: "()foo", | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		msg: `spec.resourceAttributes.labelSelector.requirements[0].key: Invalid value: "()foo": 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]')`, | ||||
| 	}, { | ||||
| 		name: "resource attributes: label selector no value for in", | ||||
| 		obj: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 			User: "me", | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 				LabelSelector: &authorizationapi.LabelSelectorAttributes{ | ||||
| 					Requirements: []metav1.LabelSelectorRequirement{ | ||||
| 						{ | ||||
| 							Key:      "k", | ||||
| 							Operator: metav1.LabelSelectorOpIn, | ||||
| 							Values:   []string{}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		msg: "spec.resourceAttributes.labelSelector.requirements[0].values: Required value: must be specified when `operator` is 'In' or 'NotIn'", | ||||
| 	}, { | ||||
| 		name: "resource attributes: label selector no value for not in", | ||||
| 		obj: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 			User: "me", | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 				LabelSelector: &authorizationapi.LabelSelectorAttributes{ | ||||
| 					Requirements: []metav1.LabelSelectorRequirement{ | ||||
| 						{ | ||||
| 							Key:      "k", | ||||
| 							Operator: metav1.LabelSelectorOpNotIn, | ||||
| 							Values:   []string{}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		msg: "spec.resourceAttributes.labelSelector.requirements[0].values: Required value: must be specified when `operator` is 'In' or 'NotIn'", | ||||
| 	}, { | ||||
| 		name: "resource attributes: label selector values for exists", | ||||
| 		obj: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 			User: "me", | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 				LabelSelector: &authorizationapi.LabelSelectorAttributes{ | ||||
| 					Requirements: []metav1.LabelSelectorRequirement{ | ||||
| 						{ | ||||
| 							Key:      "k", | ||||
| 							Operator: metav1.LabelSelectorOpExists, | ||||
| 							Values:   []string{"val"}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		msg: "spec.resourceAttributes.labelSelector.requirements[0].values: Forbidden: may not be specified when `operator` is 'Exists' or 'DoesNotExist'", | ||||
| 	}, { | ||||
| 		name: "resource attributes: label selector values for not exists", | ||||
| 		obj: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 			User: "me", | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 				LabelSelector: &authorizationapi.LabelSelectorAttributes{ | ||||
| 					Requirements: []metav1.LabelSelectorRequirement{ | ||||
| 						{ | ||||
| 							Key:      "k", | ||||
| 							Operator: metav1.LabelSelectorOpDoesNotExist, | ||||
| 							Values:   []string{"val"}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		msg: "spec.resourceAttributes.labelSelector.requirements[0].values: Forbidden: may not be specified when `operator` is 'Exists' or 'DoesNotExist'", | ||||
| 	}} | ||||
|  | ||||
| 	for _, c := range errorCases { | ||||
| 		t.Run(c.name, func(t *testing.T) { | ||||
| 			errs := ValidateSubjectAccessReviewSpec(c.obj, field.NewPath("spec")) | ||||
| 			if len(errs) == 0 { | ||||
| 				t.Errorf("%s: expected failure for %q", c.name, c.msg) | ||||
| @@ -80,7 +352,7 @@ func TestValidateSARSpec(t *testing.T) { | ||||
| 			} else if !strings.Contains(errs[0].Error(), c.msg) { | ||||
| 				t.Errorf("%s: unexpected error: %q, expected: %q", c.name, errs[0], c.msg) | ||||
| 			} | ||||
|  | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -109,6 +381,20 @@ func TestValidateSelfSAR(t *testing.T) { | ||||
| 			NonResourceAttributes: &authorizationapi.NonResourceAttributes{}, | ||||
| 		}, | ||||
| 		msg: "cannot be specified in combination with resourceAttributes", | ||||
| 	}, { | ||||
| 		// here we only test one to be sure the function is called.  The more exhaustive suite is tested above. | ||||
| 		name: "resource attributes: label selector specify both", | ||||
| 		obj: authorizationapi.SelfSubjectAccessReviewSpec{ | ||||
| 			ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 				LabelSelector: &authorizationapi.LabelSelectorAttributes{ | ||||
| 					RawSelector: "foo", | ||||
| 					Requirements: []metav1.LabelSelectorRequirement{ | ||||
| 						{}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		msg: `spec.resourceAttributes.labelSelector.rawSelector: Invalid value: "foo": may not specified at the same time as requirements`, | ||||
| 	}} | ||||
|  | ||||
| 	for _, c := range errorCases { | ||||
| @@ -175,6 +461,23 @@ func TestValidateLocalSAR(t *testing.T) { | ||||
| 			}, | ||||
| 		}, | ||||
| 		msg: "disallowed on this kind of request", | ||||
| 	}, { | ||||
| 		// here we only test one to be sure the function is called.  The more exhaustive suite is tested above. | ||||
| 		name: "resource attributes: label selector specify both", | ||||
| 		obj: &authorizationapi.LocalSubjectAccessReview{ | ||||
| 			Spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 				User: "user", | ||||
| 				ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 					LabelSelector: &authorizationapi.LabelSelectorAttributes{ | ||||
| 						RawSelector: "foo", | ||||
| 						Requirements: []metav1.LabelSelectorRequirement{ | ||||
| 							{}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		msg: `spec.resourceAttributes.labelSelector.rawSelector: Invalid value: "foo": may not specified at the same time as requirements`, | ||||
| 	}} | ||||
|  | ||||
| 	for _, c := range errorCases { | ||||
|   | ||||
| @@ -64,6 +64,8 @@ var typesAllowedTags = map[reflect.Type]bool{ | ||||
| 	reflect.TypeOf(metav1.ObjectMeta{}):               true, | ||||
| 	reflect.TypeOf(metav1.OwnerReference{}):           true, | ||||
| 	reflect.TypeOf(metav1.LabelSelector{}):            true, | ||||
| 	reflect.TypeOf(metav1.LabelSelectorRequirement{}): true, | ||||
| 	reflect.TypeOf(metav1.FieldSelectorRequirement{}): true, | ||||
| 	reflect.TypeOf(metav1.GetOptions{}):               true, | ||||
| 	reflect.TypeOf(metav1.ListOptions{}):              true, | ||||
| 	reflect.TypeOf(metav1.DeleteOptions{}):            true, | ||||
|   | ||||
| @@ -1253,6 +1253,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS | ||||
|  | ||||
| 	genericfeatures.APIServingWithRoutine: {Default: true, PreRelease: featuregate.Beta}, | ||||
|  | ||||
| 	genericfeatures.AuthorizeWithSelectors: {Default: false, PreRelease: featuregate.Alpha}, | ||||
|  | ||||
| 	genericfeatures.ConsistentListFromCache: {Default: false, PreRelease: featuregate.Alpha}, | ||||
|  | ||||
| 	genericfeatures.CustomResourceValidationExpressions: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.31 | ||||
|   | ||||
| @@ -25,7 +25,9 @@ import ( | ||||
| 	"k8s.io/apimachinery/pkg/runtime" | ||||
| 	"k8s.io/apiserver/pkg/authorization/authorizer" | ||||
| 	genericapirequest "k8s.io/apiserver/pkg/endpoints/request" | ||||
| 	genericfeatures "k8s.io/apiserver/pkg/features" | ||||
| 	"k8s.io/apiserver/pkg/registry/rest" | ||||
| 	utilfeature "k8s.io/apiserver/pkg/util/feature" | ||||
| 	authorizationapi "k8s.io/kubernetes/pkg/apis/authorization" | ||||
| 	authorizationvalidation "k8s.io/kubernetes/pkg/apis/authorization/validation" | ||||
| 	authorizationutil "k8s.io/kubernetes/pkg/registry/authorization/util" | ||||
| @@ -64,6 +66,14 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation | ||||
| 	if !ok { | ||||
| 		return nil, apierrors.NewBadRequest(fmt.Sprintf("not a LocaLocalSubjectAccessReview: %#v", obj)) | ||||
| 	} | ||||
| 	// clear fields if the featuregate is disabled | ||||
| 	if !utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) { | ||||
| 		if localSubjectAccessReview.Spec.ResourceAttributes != nil { | ||||
| 			localSubjectAccessReview.Spec.ResourceAttributes.FieldSelector = nil | ||||
| 			localSubjectAccessReview.Spec.ResourceAttributes.LabelSelector = nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if errs := authorizationvalidation.ValidateLocalSubjectAccessReview(localSubjectAccessReview); len(errs) > 0 { | ||||
| 		return nil, apierrors.NewInvalid(authorizationapi.Kind(localSubjectAccessReview.Kind), "", errs) | ||||
| 	} | ||||
| @@ -89,9 +99,7 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation | ||||
| 		Denied:  (decision == authorizer.DecisionDeny), | ||||
| 		Reason:  reason, | ||||
| 	} | ||||
| 	if evaluationErr != nil { | ||||
| 		localSubjectAccessReview.Status.EvaluationError = evaluationErr.Error() | ||||
| 	} | ||||
| 	localSubjectAccessReview.Status.EvaluationError = authorizationutil.BuildEvaluationError(evaluationErr, authorizationAttributes) | ||||
|  | ||||
| 	return localSubjectAccessReview, nil | ||||
| } | ||||
|   | ||||
| @@ -25,7 +25,9 @@ import ( | ||||
| 	"k8s.io/apimachinery/pkg/runtime" | ||||
| 	"k8s.io/apiserver/pkg/authorization/authorizer" | ||||
| 	genericapirequest "k8s.io/apiserver/pkg/endpoints/request" | ||||
| 	genericfeatures "k8s.io/apiserver/pkg/features" | ||||
| 	"k8s.io/apiserver/pkg/registry/rest" | ||||
| 	utilfeature "k8s.io/apiserver/pkg/util/feature" | ||||
| 	authorizationapi "k8s.io/kubernetes/pkg/apis/authorization" | ||||
| 	authorizationvalidation "k8s.io/kubernetes/pkg/apis/authorization/validation" | ||||
| 	authorizationutil "k8s.io/kubernetes/pkg/registry/authorization/util" | ||||
| @@ -64,6 +66,13 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation | ||||
| 	if !ok { | ||||
| 		return nil, apierrors.NewBadRequest(fmt.Sprintf("not a SelfSubjectAccessReview: %#v", obj)) | ||||
| 	} | ||||
| 	// clear fields if the featuregate is disabled | ||||
| 	if !utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) { | ||||
| 		if selfSAR.Spec.ResourceAttributes != nil { | ||||
| 			selfSAR.Spec.ResourceAttributes.FieldSelector = nil | ||||
| 			selfSAR.Spec.ResourceAttributes.LabelSelector = nil | ||||
| 		} | ||||
| 	} | ||||
| 	if errs := authorizationvalidation.ValidateSelfSubjectAccessReview(selfSAR); len(errs) > 0 { | ||||
| 		return nil, apierrors.NewInvalid(authorizationapi.Kind(selfSAR.Kind), "", errs) | ||||
| 	} | ||||
| @@ -92,9 +101,7 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation | ||||
| 		Denied:  (decision == authorizer.DecisionDeny), | ||||
| 		Reason:  reason, | ||||
| 	} | ||||
| 	if evaluationErr != nil { | ||||
| 		selfSAR.Status.EvaluationError = evaluationErr.Error() | ||||
| 	} | ||||
| 	selfSAR.Status.EvaluationError = authorizationutil.BuildEvaluationError(evaluationErr, authorizationAttributes) | ||||
|  | ||||
| 	return selfSAR, nil | ||||
| } | ||||
|   | ||||
| @@ -24,7 +24,9 @@ import ( | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/runtime" | ||||
| 	"k8s.io/apiserver/pkg/authorization/authorizer" | ||||
| 	genericfeatures "k8s.io/apiserver/pkg/features" | ||||
| 	"k8s.io/apiserver/pkg/registry/rest" | ||||
| 	utilfeature "k8s.io/apiserver/pkg/util/feature" | ||||
| 	authorizationapi "k8s.io/kubernetes/pkg/apis/authorization" | ||||
| 	authorizationvalidation "k8s.io/kubernetes/pkg/apis/authorization/validation" | ||||
| 	authorizationutil "k8s.io/kubernetes/pkg/registry/authorization/util" | ||||
| @@ -63,6 +65,13 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation | ||||
| 	if !ok { | ||||
| 		return nil, apierrors.NewBadRequest(fmt.Sprintf("not a SubjectAccessReview: %#v", obj)) | ||||
| 	} | ||||
| 	// clear fields if the featuregate is disabled | ||||
| 	if !utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) { | ||||
| 		if subjectAccessReview.Spec.ResourceAttributes != nil { | ||||
| 			subjectAccessReview.Spec.ResourceAttributes.FieldSelector = nil | ||||
| 			subjectAccessReview.Spec.ResourceAttributes.LabelSelector = nil | ||||
| 		} | ||||
| 	} | ||||
| 	if errs := authorizationvalidation.ValidateSubjectAccessReview(subjectAccessReview); len(errs) > 0 { | ||||
| 		return nil, apierrors.NewInvalid(authorizationapi.Kind(subjectAccessReview.Kind), "", errs) | ||||
| 	} | ||||
| @@ -81,9 +90,7 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation | ||||
| 		Denied:  (decision == authorizer.DecisionDeny), | ||||
| 		Reason:  reason, | ||||
| 	} | ||||
| 	if evaluationErr != nil { | ||||
| 		subjectAccessReview.Status.EvaluationError = evaluationErr.Error() | ||||
| 	} | ||||
| 	subjectAccessReview.Status.EvaluationError = authorizationutil.BuildEvaluationError(evaluationErr, authorizationAttributes) | ||||
|  | ||||
| 	return subjectAccessReview, nil | ||||
| } | ||||
|   | ||||
| @@ -24,10 +24,15 @@ import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/fields" | ||||
| 	"k8s.io/apimachinery/pkg/labels" | ||||
| 	"k8s.io/apiserver/pkg/authentication/user" | ||||
| 	"k8s.io/apiserver/pkg/authorization/authorizer" | ||||
| 	genericapirequest "k8s.io/apiserver/pkg/endpoints/request" | ||||
| 	genericfeatures "k8s.io/apiserver/pkg/features" | ||||
| 	"k8s.io/apiserver/pkg/registry/rest" | ||||
| 	utilfeature "k8s.io/apiserver/pkg/util/feature" | ||||
| 	featuregatetesting "k8s.io/component-base/featuregate/testing" | ||||
| 	authorizationapi "k8s.io/kubernetes/pkg/apis/authorization" | ||||
| ) | ||||
|  | ||||
| @@ -187,8 +192,52 @@ func TestCreate(t *testing.T) { | ||||
| 				Denied:  true, | ||||
| 			}, | ||||
| 		}, | ||||
|  | ||||
| 		"resource denied, valid selectors": { | ||||
| 			spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 				User: "bob", | ||||
| 				ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 					FieldSelector: &authorizationapi.FieldSelectorAttributes{RawSelector: "foo=bar"}, | ||||
| 					LabelSelector: &authorizationapi.LabelSelectorAttributes{RawSelector: "key=value"}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			decision: authorizer.DecisionDeny, | ||||
| 			expectedAttrs: authorizer.AttributesRecord{ | ||||
| 				User:                      &user.DefaultInfo{Name: "bob"}, | ||||
| 				ResourceRequest:           true, | ||||
| 				APIVersion:                "*", | ||||
| 				FieldSelectorRequirements: fields.Requirements{{Operator: "=", Field: "foo", Value: "bar"}}, | ||||
| 				LabelSelectorRequirements: mustParse("key=value"), | ||||
| 			}, | ||||
| 			expectedStatus: authorizationapi.SubjectAccessReviewStatus{ | ||||
| 				Allowed: false, | ||||
| 				Denied:  true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"resource denied, invalid selectors": { | ||||
| 			spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 				User: "bob", | ||||
| 				ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 					FieldSelector: &authorizationapi.FieldSelectorAttributes{RawSelector: "key in value"}, | ||||
| 					LabelSelector: &authorizationapi.LabelSelectorAttributes{RawSelector: "&"}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			decision: authorizer.DecisionDeny, | ||||
| 			expectedAttrs: authorizer.AttributesRecord{ | ||||
| 				User:            &user.DefaultInfo{Name: "bob"}, | ||||
| 				ResourceRequest: true, | ||||
| 				APIVersion:      "*", | ||||
| 			}, | ||||
| 			expectedStatus: authorizationapi.SubjectAccessReviewStatus{ | ||||
| 				Allowed:         false, | ||||
| 				Denied:          true, | ||||
| 				EvaluationError: `spec.resourceAttributes.fieldSelector ignored due to parse error; spec.resourceAttributes.labelSelector ignored due to parse error`, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true) | ||||
|  | ||||
| 	for k, tc := range testcases { | ||||
| 		auth := &fakeAuthorizer{ | ||||
| 			decision: tc.decision, | ||||
| @@ -208,8 +257,13 @@ func TestCreate(t *testing.T) { | ||||
| 			} | ||||
| 			continue | ||||
| 		} | ||||
| 		if !reflect.DeepEqual(auth.attrs, tc.expectedAttrs) { | ||||
| 			t.Errorf("%s: expected\n%#v\ngot\n%#v", k, tc.expectedAttrs, auth.attrs) | ||||
| 		gotAttrs := auth.attrs.(authorizer.AttributesRecord) | ||||
| 		if tc.expectedStatus.EvaluationError != "" { | ||||
| 			gotAttrs.FieldSelectorParsingErr = nil | ||||
| 			gotAttrs.LabelSelectorParsingErr = nil | ||||
| 		} | ||||
| 		if !reflect.DeepEqual(gotAttrs, tc.expectedAttrs) { | ||||
| 			t.Errorf("%s: expected\n%#v\ngot\n%#v", k, tc.expectedAttrs, gotAttrs) | ||||
| 		} | ||||
| 		status := result.(*authorizationapi.SubjectAccessReview).Status | ||||
| 		if !reflect.DeepEqual(status, tc.expectedStatus) { | ||||
| @@ -217,3 +271,12 @@ func TestCreate(t *testing.T) { | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func mustParse(s string) labels.Requirements { | ||||
| 	selector, err := labels.Parse(s) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	reqs, _ := selector.Requirements() | ||||
| 	return reqs | ||||
| } | ||||
|   | ||||
| @@ -17,14 +17,24 @@ limitations under the License. | ||||
| package util | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/fields" | ||||
| 	"k8s.io/apimachinery/pkg/labels" | ||||
| 	"k8s.io/apimachinery/pkg/selection" | ||||
| 	utilerrors "k8s.io/apimachinery/pkg/util/errors" | ||||
| 	"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" | ||||
| 	authorizationapi "k8s.io/kubernetes/pkg/apis/authorization" | ||||
| ) | ||||
|  | ||||
| // ResourceAttributesFrom combines the API object information and the user.Info from the context to build a full authorizer.AttributesRecord for resource access | ||||
| func ResourceAttributesFrom(user user.Info, in authorizationapi.ResourceAttributes) authorizer.AttributesRecord { | ||||
| 	return authorizer.AttributesRecord{ | ||||
| 	ret := authorizer.AttributesRecord{ | ||||
| 		User:            user, | ||||
| 		Verb:            in.Verb, | ||||
| 		Namespace:       in.Namespace, | ||||
| @@ -35,6 +45,129 @@ func ResourceAttributesFrom(user user.Info, in authorizationapi.ResourceAttribut | ||||
| 		Name:            in.Name, | ||||
| 		ResourceRequest: true, | ||||
| 	} | ||||
|  | ||||
| 	if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) { | ||||
| 		if in.LabelSelector != nil { | ||||
| 			if len(in.LabelSelector.RawSelector) > 0 { | ||||
| 				labelSelector, err := labels.Parse(in.LabelSelector.RawSelector) | ||||
| 				if err != nil { | ||||
| 					ret.LabelSelectorRequirements, ret.LabelSelectorParsingErr = nil, err | ||||
| 				} else { | ||||
| 					requirements, _ /*selectable*/ := labelSelector.Requirements() | ||||
| 					ret.LabelSelectorRequirements, ret.LabelSelectorParsingErr = requirements, nil | ||||
| 				} | ||||
| 			} | ||||
| 			if len(in.LabelSelector.Requirements) > 0 { | ||||
| 				ret.LabelSelectorRequirements, ret.LabelSelectorParsingErr = labelSelectorAsSelector(in.LabelSelector.Requirements) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if in.FieldSelector != nil { | ||||
| 			if len(in.FieldSelector.RawSelector) > 0 { | ||||
| 				fieldSelector, err := fields.ParseSelector(in.FieldSelector.RawSelector) | ||||
| 				if err != nil { | ||||
| 					ret.FieldSelectorRequirements, ret.FieldSelectorParsingErr = nil, err | ||||
| 				} else { | ||||
| 					ret.FieldSelectorRequirements, ret.FieldSelectorParsingErr = fieldSelector.Requirements(), nil | ||||
| 				} | ||||
| 			} | ||||
| 			if len(in.FieldSelector.Requirements) > 0 { | ||||
| 				ret.FieldSelectorRequirements, ret.FieldSelectorParsingErr = fieldSelectorAsSelector(in.FieldSelector.Requirements) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return ret | ||||
| } | ||||
|  | ||||
| var labelSelectorOpToSelectionOp = map[metav1.LabelSelectorOperator]selection.Operator{ | ||||
| 	metav1.LabelSelectorOpIn:           selection.In, | ||||
| 	metav1.LabelSelectorOpNotIn:        selection.NotIn, | ||||
| 	metav1.LabelSelectorOpExists:       selection.Exists, | ||||
| 	metav1.LabelSelectorOpDoesNotExist: selection.DoesNotExist, | ||||
| } | ||||
|  | ||||
| func labelSelectorAsSelector(requirements []metav1.LabelSelectorRequirement) (labels.Requirements, error) { | ||||
| 	if len(requirements) == 0 { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	reqs := make([]labels.Requirement, 0, len(requirements)) | ||||
| 	var errs []error | ||||
| 	for _, expr := range requirements { | ||||
| 		op, ok := labelSelectorOpToSelectionOp[expr.Operator] | ||||
| 		if !ok { | ||||
| 			errs = append(errs, fmt.Errorf("%q is not a valid label selector operator", expr.Operator)) | ||||
| 			continue | ||||
| 		} | ||||
| 		values := expr.Values | ||||
| 		if len(values) == 0 { | ||||
| 			values = nil | ||||
| 		} | ||||
| 		req, err := labels.NewRequirement(expr.Key, op, values) | ||||
| 		if err != nil { | ||||
| 			errs = append(errs, err) | ||||
| 			continue | ||||
| 		} | ||||
| 		reqs = append(reqs, *req) | ||||
| 	} | ||||
|  | ||||
| 	// If this happens, it means all requirements ended up getting skipped. | ||||
| 	// Return nil rather than []. | ||||
| 	if len(reqs) == 0 { | ||||
| 		reqs = nil | ||||
| 	} | ||||
|  | ||||
| 	// Return any accumulated errors along with any accumulated requirements, so recognized / valid requirements can be considered by authorization. | ||||
| 	// This is safe because requirements are ANDed together so dropping unknown / invalid ones results in a strictly broader authorization check. | ||||
| 	return labels.Requirements(reqs), utilerrors.NewAggregate(errs) | ||||
| } | ||||
|  | ||||
| func fieldSelectorAsSelector(requirements []metav1.FieldSelectorRequirement) (fields.Requirements, error) { | ||||
| 	if len(requirements) == 0 { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	reqs := make([]fields.Requirement, 0, len(requirements)) | ||||
| 	var errs []error | ||||
| 	for _, expr := range requirements { | ||||
| 		if len(expr.Values) > 1 { | ||||
| 			errs = append(errs, fmt.Errorf("fieldSelectors do not yet support multiple values")) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		switch expr.Operator { | ||||
| 		case metav1.FieldSelectorOpIn: | ||||
| 			if len(expr.Values) != 1 { | ||||
| 				errs = append(errs, fmt.Errorf("fieldSelectors in must have one value")) | ||||
| 				continue | ||||
| 			} | ||||
| 			// when converting to fields.Requirement, use Equals to match how parsed field selectors behave | ||||
| 			reqs = append(reqs, fields.Requirement{Field: expr.Key, Operator: selection.Equals, Value: expr.Values[0]}) | ||||
| 		case metav1.FieldSelectorOpNotIn: | ||||
| 			if len(expr.Values) != 1 { | ||||
| 				errs = append(errs, fmt.Errorf("fieldSelectors not in must have one value")) | ||||
| 				continue | ||||
| 			} | ||||
| 			// when converting to fields.Requirement, use NotEquals to match how parsed field selectors behave | ||||
| 			reqs = append(reqs, fields.Requirement{Field: expr.Key, Operator: selection.NotEquals, Value: expr.Values[0]}) | ||||
| 		case metav1.FieldSelectorOpExists, metav1.FieldSelectorOpDoesNotExist: | ||||
| 			errs = append(errs, fmt.Errorf("fieldSelectors do not yet support %v", expr.Operator)) | ||||
| 			continue | ||||
| 		default: | ||||
| 			errs = append(errs, fmt.Errorf("%q is not a valid field selector operator", expr.Operator)) | ||||
| 			continue | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// If this happens, it means all requirements ended up getting skipped. | ||||
| 	// Return nil rather than []. | ||||
| 	if len(reqs) == 0 { | ||||
| 		reqs = nil | ||||
| 	} | ||||
|  | ||||
| 	// Return any accumulated errors along with any accumulated requirements, so recognized / valid requirements can be considered by authorization. | ||||
| 	// This is safe because requirements are ANDed together so dropping unknown / invalid ones results in a strictly broader authorization check. | ||||
| 	return fields.Requirements(reqs), utilerrors.NewAggregate(errs) | ||||
| } | ||||
|  | ||||
| // NonResourceAttributesFrom combines the API object information and the user.Info from the context to build a full authorizer.AttributesRecord for non resource access | ||||
| @@ -85,3 +218,27 @@ func matchAllVersionIfEmpty(version string) string { | ||||
| 	} | ||||
| 	return version | ||||
| } | ||||
|  | ||||
| // BuildEvaluationError constructs the evaluation error string to include in *SubjectAccessReview status | ||||
| // based on the authorizer evaluation error and any field and label selector parse errors. | ||||
| func BuildEvaluationError(evaluationError error, attrs authorizer.AttributesRecord) string { | ||||
| 	var evaluationErrors []string | ||||
| 	if evaluationError != nil { | ||||
| 		evaluationErrors = append(evaluationErrors, evaluationError.Error()) | ||||
| 	} | ||||
| 	if reqs, err := attrs.GetFieldSelector(); err != nil { | ||||
| 		if len(reqs) > 0 { | ||||
| 			evaluationErrors = append(evaluationErrors, "spec.resourceAttributes.fieldSelector partially ignored due to parse error") | ||||
| 		} else { | ||||
| 			evaluationErrors = append(evaluationErrors, "spec.resourceAttributes.fieldSelector ignored due to parse error") | ||||
| 		} | ||||
| 	} | ||||
| 	if reqs, err := attrs.GetLabelSelector(); err != nil { | ||||
| 		if len(reqs) > 0 { | ||||
| 			evaluationErrors = append(evaluationErrors, "spec.resourceAttributes.labelSelector partially ignored due to parse error") | ||||
| 		} else { | ||||
| 			evaluationErrors = append(evaluationErrors, "spec.resourceAttributes.labelSelector ignored due to parse error") | ||||
| 		} | ||||
| 	} | ||||
| 	return strings.Join(evaluationErrors, "; ") | ||||
| } | ||||
|   | ||||
| @@ -17,12 +17,21 @@ limitations under the License. | ||||
| package util | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
|  | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/fields" | ||||
| 	"k8s.io/apimachinery/pkg/labels" | ||||
| 	"k8s.io/apimachinery/pkg/selection" | ||||
| 	utilerrors "k8s.io/apimachinery/pkg/util/errors" | ||||
| 	"k8s.io/apimachinery/pkg/util/sets" | ||||
| 	"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" | ||||
| 	authorizationapi "k8s.io/kubernetes/pkg/apis/authorization" | ||||
| ) | ||||
|  | ||||
| @@ -37,6 +46,10 @@ func TestResourceAttributesFrom(t *testing.T) { | ||||
| 		"Subresource", | ||||
| 		"Name", | ||||
|  | ||||
| 		// Fields we read and parse in ResourceAttributesFrom | ||||
| 		"FieldSelector", | ||||
| 		"LabelSelector", | ||||
|  | ||||
| 		// Fields we copy in NonResourceAttributesFrom | ||||
| 		"Path", | ||||
| 		"Verb", | ||||
| @@ -60,6 +73,12 @@ func TestResourceAttributesFrom(t *testing.T) { | ||||
| 		"Name", | ||||
| 		"ResourceRequest", | ||||
|  | ||||
| 		// Fields we compute and set in ResourceAttributesFrom | ||||
| 		"FieldSelectorRequirements", | ||||
| 		"FieldSelectorParsingErr", | ||||
| 		"LabelSelectorRequirements", | ||||
| 		"LabelSelectorParsingErr", | ||||
|  | ||||
| 		// Fields we set in NonResourceAttributesFrom | ||||
| 		"User", | ||||
| 		"ResourceRequest", | ||||
| @@ -75,6 +94,14 @@ func TestResourceAttributesFrom(t *testing.T) { | ||||
| } | ||||
|  | ||||
| func TestAuthorizationAttributesFrom(t *testing.T) { | ||||
| 	mustRequirement := func(key string, op selection.Operator, vals []string) labels.Requirement { | ||||
| 		ret, err := labels.NewRequirement(key, op, vals) | ||||
| 		if err != nil { | ||||
| 			panic(err) | ||||
| 		} | ||||
| 		return *ret | ||||
| 	} | ||||
|  | ||||
| 	type args struct { | ||||
| 		spec authorizationapi.SubjectAccessReviewSpec | ||||
| 	} | ||||
| @@ -82,6 +109,7 @@ func TestAuthorizationAttributesFrom(t *testing.T) { | ||||
| 		name                        string | ||||
| 		args                        args | ||||
| 		want                        authorizer.AttributesRecord | ||||
| 		enableAuthorizationSelector bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "nonresource", | ||||
| @@ -162,11 +190,461 @@ func TestAuthorizationAttributesFrom(t *testing.T) { | ||||
| 				ResourceRequest: true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "field: ignore when featuregate off", | ||||
| 			args: args{ | ||||
| 				spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 					ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 						FieldSelector: &authorizationapi.FieldSelectorAttributes{ | ||||
| 							RawSelector: "foo=bar", | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: authorizer.AttributesRecord{ | ||||
| 				User:            &user.DefaultInfo{}, | ||||
| 				ResourceRequest: true, | ||||
| 				APIVersion:      "*", | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "field: raw selector", | ||||
| 			args: args{ | ||||
| 				spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 					ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 						FieldSelector: &authorizationapi.FieldSelectorAttributes{ | ||||
| 							RawSelector: "foo=bar", | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: authorizer.AttributesRecord{ | ||||
| 				User:            &user.DefaultInfo{}, | ||||
| 				ResourceRequest: true, | ||||
| 				APIVersion:      "*", | ||||
| 				FieldSelectorRequirements: fields.Requirements{ | ||||
| 					{Operator: "=", Field: "foo", Value: "bar"}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			enableAuthorizationSelector: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "field: raw selector error", | ||||
| 			args: args{ | ||||
| 				spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 					ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 						FieldSelector: &authorizationapi.FieldSelectorAttributes{ | ||||
| 							RawSelector: "&foo", | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: authorizer.AttributesRecord{ | ||||
| 				User:                    &user.DefaultInfo{}, | ||||
| 				ResourceRequest:         true, | ||||
| 				APIVersion:              "*", | ||||
| 				FieldSelectorParsingErr: errors.New("invalid selector: '&foo'; can't understand '&foo'"), | ||||
| 			}, | ||||
| 			enableAuthorizationSelector: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "field: requirements", | ||||
| 			args: args{ | ||||
| 				spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 					ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 						FieldSelector: &authorizationapi.FieldSelectorAttributes{ | ||||
| 							Requirements: []metav1.FieldSelectorRequirement{ | ||||
| 								{ | ||||
| 									Key:      "one", | ||||
| 									Operator: "In", | ||||
| 									Values:   []string{"apple"}, | ||||
| 								}, | ||||
| 								{ | ||||
| 									Key:      "two", | ||||
| 									Operator: "NotIn", | ||||
| 									Values:   []string{"banana"}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: authorizer.AttributesRecord{ | ||||
| 				User:            &user.DefaultInfo{}, | ||||
| 				ResourceRequest: true, | ||||
| 				APIVersion:      "*", | ||||
| 				FieldSelectorRequirements: fields.Requirements{ | ||||
| 					{Operator: "=", Field: "one", Value: "apple"}, | ||||
| 					{Operator: "!=", Field: "two", Value: "banana"}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			enableAuthorizationSelector: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "field: requirements too many values", | ||||
| 			args: args{ | ||||
| 				spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 					ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 						FieldSelector: &authorizationapi.FieldSelectorAttributes{ | ||||
| 							Requirements: []metav1.FieldSelectorRequirement{ | ||||
| 								{ | ||||
| 									Key:      "one", | ||||
| 									Operator: "In", | ||||
| 									Values:   []string{"apple", "other"}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: authorizer.AttributesRecord{ | ||||
| 				User:                    &user.DefaultInfo{}, | ||||
| 				ResourceRequest:         true, | ||||
| 				APIVersion:              "*", | ||||
| 				FieldSelectorParsingErr: utilerrors.NewAggregate([]error{errors.New("fieldSelectors do not yet support multiple values")}), | ||||
| 			}, | ||||
| 			enableAuthorizationSelector: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "field: requirements missing in value", | ||||
| 			args: args{ | ||||
| 				spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 					ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 						FieldSelector: &authorizationapi.FieldSelectorAttributes{ | ||||
| 							Requirements: []metav1.FieldSelectorRequirement{ | ||||
| 								{ | ||||
| 									Key:      "one", | ||||
| 									Operator: "In", | ||||
| 									Values:   []string{}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: authorizer.AttributesRecord{ | ||||
| 				User:                    &user.DefaultInfo{}, | ||||
| 				ResourceRequest:         true, | ||||
| 				APIVersion:              "*", | ||||
| 				FieldSelectorParsingErr: utilerrors.NewAggregate([]error{errors.New("fieldSelectors in must have one value")}), | ||||
| 			}, | ||||
| 			enableAuthorizationSelector: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "field: requirements missing notin value", | ||||
| 			args: args{ | ||||
| 				spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 					ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 						FieldSelector: &authorizationapi.FieldSelectorAttributes{ | ||||
| 							Requirements: []metav1.FieldSelectorRequirement{ | ||||
| 								{ | ||||
| 									Key:      "one", | ||||
| 									Operator: "NotIn", | ||||
| 									Values:   []string{}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: authorizer.AttributesRecord{ | ||||
| 				User:                    &user.DefaultInfo{}, | ||||
| 				ResourceRequest:         true, | ||||
| 				APIVersion:              "*", | ||||
| 				FieldSelectorParsingErr: utilerrors.NewAggregate([]error{errors.New("fieldSelectors not in must have one value")}), | ||||
| 			}, | ||||
| 			enableAuthorizationSelector: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "field: requirements exists", | ||||
| 			args: args{ | ||||
| 				spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 					ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 						FieldSelector: &authorizationapi.FieldSelectorAttributes{ | ||||
| 							Requirements: []metav1.FieldSelectorRequirement{ | ||||
| 								{ | ||||
| 									Key:      "one", | ||||
| 									Operator: "Exists", | ||||
| 									Values:   []string{}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: authorizer.AttributesRecord{ | ||||
| 				User:                    &user.DefaultInfo{}, | ||||
| 				ResourceRequest:         true, | ||||
| 				APIVersion:              "*", | ||||
| 				FieldSelectorParsingErr: utilerrors.NewAggregate([]error{errors.New("fieldSelectors do not yet support Exists")}), | ||||
| 			}, | ||||
| 			enableAuthorizationSelector: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "field: requirements DoesNotExist", | ||||
| 			args: args{ | ||||
| 				spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 					ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 						FieldSelector: &authorizationapi.FieldSelectorAttributes{ | ||||
| 							Requirements: []metav1.FieldSelectorRequirement{ | ||||
| 								{ | ||||
| 									Key:      "one", | ||||
| 									Operator: "DoesNotExist", | ||||
| 									Values:   []string{}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: authorizer.AttributesRecord{ | ||||
| 				User:                    &user.DefaultInfo{}, | ||||
| 				ResourceRequest:         true, | ||||
| 				APIVersion:              "*", | ||||
| 				FieldSelectorParsingErr: utilerrors.NewAggregate([]error{errors.New("fieldSelectors do not yet support DoesNotExist")}), | ||||
| 			}, | ||||
| 			enableAuthorizationSelector: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "field: requirements bad operator", | ||||
| 			args: args{ | ||||
| 				spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 					ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 						FieldSelector: &authorizationapi.FieldSelectorAttributes{ | ||||
| 							Requirements: []metav1.FieldSelectorRequirement{ | ||||
| 								{ | ||||
| 									Key:      "one", | ||||
| 									Operator: "bad", | ||||
| 									Values:   []string{}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: authorizer.AttributesRecord{ | ||||
| 				User:                    &user.DefaultInfo{}, | ||||
| 				ResourceRequest:         true, | ||||
| 				APIVersion:              "*", | ||||
| 				FieldSelectorParsingErr: utilerrors.NewAggregate([]error{errors.New("\"bad\" is not a valid field selector operator")}), | ||||
| 			}, | ||||
| 			enableAuthorizationSelector: true, | ||||
| 		}, | ||||
|  | ||||
| 		{ | ||||
| 			name: "label: ignore when featuregate off", | ||||
| 			args: args{ | ||||
| 				spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 					ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 						LabelSelector: &authorizationapi.LabelSelectorAttributes{ | ||||
| 							RawSelector: "foo=bar", | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: authorizer.AttributesRecord{ | ||||
| 				User:            &user.DefaultInfo{}, | ||||
| 				ResourceRequest: true, | ||||
| 				APIVersion:      "*", | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "label: raw selector", | ||||
| 			args: args{ | ||||
| 				spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 					ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 						LabelSelector: &authorizationapi.LabelSelectorAttributes{ | ||||
| 							RawSelector: "foo=bar", | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: authorizer.AttributesRecord{ | ||||
| 				User:            &user.DefaultInfo{}, | ||||
| 				ResourceRequest: true, | ||||
| 				APIVersion:      "*", | ||||
| 				LabelSelectorRequirements: labels.Requirements{ | ||||
| 					mustRequirement("foo", "=", []string{"bar"}), | ||||
| 				}, | ||||
| 			}, | ||||
| 			enableAuthorizationSelector: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "label: raw selector error", | ||||
| 			args: args{ | ||||
| 				spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 					ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 						LabelSelector: &authorizationapi.LabelSelectorAttributes{ | ||||
| 							RawSelector: "&foo", | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: authorizer.AttributesRecord{ | ||||
| 				User:                    &user.DefaultInfo{}, | ||||
| 				ResourceRequest:         true, | ||||
| 				APIVersion:              "*", | ||||
| 				LabelSelectorParsingErr: errors.New("unable to parse requirement: <nil>: Invalid value: \"&foo\": 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, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "label: requirements", | ||||
| 			args: args{ | ||||
| 				spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 					ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 						LabelSelector: &authorizationapi.LabelSelectorAttributes{ | ||||
| 							Requirements: []metav1.LabelSelectorRequirement{ | ||||
| 								{ | ||||
| 									Key:      "one", | ||||
| 									Operator: "In", | ||||
| 									Values:   []string{"apple"}, | ||||
| 								}, | ||||
| 								{ | ||||
| 									Key:      "two", | ||||
| 									Operator: "NotIn", | ||||
| 									Values:   []string{"banana"}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: authorizer.AttributesRecord{ | ||||
| 				User:            &user.DefaultInfo{}, | ||||
| 				ResourceRequest: true, | ||||
| 				APIVersion:      "*", | ||||
| 				LabelSelectorRequirements: labels.Requirements{ | ||||
| 					mustRequirement("one", "in", []string{"apple"}), | ||||
| 					mustRequirement("two", "notin", []string{"banana"}), | ||||
| 				}, | ||||
| 			}, | ||||
| 			enableAuthorizationSelector: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "label: requirements multiple values", | ||||
| 			args: args{ | ||||
| 				spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 					ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 						LabelSelector: &authorizationapi.LabelSelectorAttributes{ | ||||
| 							Requirements: []metav1.LabelSelectorRequirement{ | ||||
| 								{ | ||||
| 									Key:      "one", | ||||
| 									Operator: "In", | ||||
| 									Values:   []string{"apple", "other"}, | ||||
| 								}, | ||||
| 								{ | ||||
| 									Key:      "two", | ||||
| 									Operator: "NotIn", | ||||
| 									Values:   []string{"carrot", "donut"}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: authorizer.AttributesRecord{ | ||||
| 				User:            &user.DefaultInfo{}, | ||||
| 				ResourceRequest: true, | ||||
| 				APIVersion:      "*", | ||||
| 				LabelSelectorRequirements: labels.Requirements{ | ||||
| 					mustRequirement("one", "in", []string{"apple", "other"}), | ||||
| 					mustRequirement("two", "notin", []string{"carrot", "donut"}), | ||||
| 				}, | ||||
| 			}, | ||||
| 			enableAuthorizationSelector: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "label: requirements exists", | ||||
| 			args: args{ | ||||
| 				spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 					ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 						LabelSelector: &authorizationapi.LabelSelectorAttributes{ | ||||
| 							Requirements: []metav1.LabelSelectorRequirement{ | ||||
| 								{ | ||||
| 									Key:      "one", | ||||
| 									Operator: "Exists", | ||||
| 									Values:   []string{}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: authorizer.AttributesRecord{ | ||||
| 				User:            &user.DefaultInfo{}, | ||||
| 				ResourceRequest: true, | ||||
| 				APIVersion:      "*", | ||||
| 				LabelSelectorRequirements: labels.Requirements{ | ||||
| 					mustRequirement("one", "exists", nil), | ||||
| 				}, | ||||
| 			}, | ||||
| 			enableAuthorizationSelector: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "label: requirements DoesNotExist", | ||||
| 			args: args{ | ||||
| 				spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 					ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 						LabelSelector: &authorizationapi.LabelSelectorAttributes{ | ||||
| 							Requirements: []metav1.LabelSelectorRequirement{ | ||||
| 								{ | ||||
| 									Key:      "one", | ||||
| 									Operator: "DoesNotExist", | ||||
| 									Values:   []string{}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: authorizer.AttributesRecord{ | ||||
| 				User:            &user.DefaultInfo{}, | ||||
| 				ResourceRequest: true, | ||||
| 				APIVersion:      "*", | ||||
| 				LabelSelectorRequirements: labels.Requirements{ | ||||
| 					mustRequirement("one", "!", nil), | ||||
| 				}, | ||||
| 			}, | ||||
| 			enableAuthorizationSelector: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "label: requirements bad operator", | ||||
| 			args: args{ | ||||
| 				spec: authorizationapi.SubjectAccessReviewSpec{ | ||||
| 					ResourceAttributes: &authorizationapi.ResourceAttributes{ | ||||
| 						LabelSelector: &authorizationapi.LabelSelectorAttributes{ | ||||
| 							Requirements: []metav1.LabelSelectorRequirement{ | ||||
| 								{ | ||||
| 									Key:      "one", | ||||
| 									Operator: "bad", | ||||
| 									Values:   []string{}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: authorizer.AttributesRecord{ | ||||
| 				User:                    &user.DefaultInfo{}, | ||||
| 				ResourceRequest:         true, | ||||
| 				APIVersion:              "*", | ||||
| 				LabelSelectorParsingErr: utilerrors.NewAggregate([]error{errors.New("\"bad\" is not a valid label selector operator")}), | ||||
| 			}, | ||||
| 			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 := AuthorizationAttributesFrom(tt.args.spec); !reflect.DeepEqual(got, tt.want) { | ||||
| 				t.Errorf("AuthorizationAttributesFrom() = %v, want %v", got, tt.want) | ||||
| 				if got.LabelSelectorParsingErr != nil { | ||||
| 					t.Logf("labelSelectorErr=%q", got.LabelSelectorParsingErr) | ||||
| 				} | ||||
| 				t.Errorf("AuthorizationAttributesFrom(), got:\n%#v\nwant:\n%#v", got, tt.want) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
|   | ||||
| @@ -24,6 +24,8 @@ import ( | ||||
|  | ||||
| 	rbacv1 "k8s.io/api/rbac/v1" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/fields" | ||||
| 	"k8s.io/apimachinery/pkg/labels" | ||||
| 	"k8s.io/apiserver/pkg/authentication/user" | ||||
| 	"k8s.io/apiserver/pkg/authorization/authorizer" | ||||
| 	rbacv1helpers "k8s.io/kubernetes/pkg/apis/rbac/v1" | ||||
| @@ -135,6 +137,12 @@ func (d *defaultAttributes) GetAPIGroup() string     { return d.apiGroup } | ||||
| func (d *defaultAttributes) GetAPIVersion() string   { return "" } | ||||
| func (d *defaultAttributes) IsResourceRequest() bool { return true } | ||||
| func (d *defaultAttributes) GetPath() string         { return "" } | ||||
| func (d *defaultAttributes) GetFieldSelector() (fields.Requirements, error) { | ||||
| 	panic("not supported for RBAC") | ||||
| } | ||||
| func (d *defaultAttributes) GetLabelSelector() (labels.Requirements, error) { | ||||
| 	panic("not supported for RBAC") | ||||
| } | ||||
|  | ||||
| func TestAuthorizer(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| @@ -263,135 +271,139 @@ func TestAuthorizer(t *testing.T) { | ||||
| } | ||||
|  | ||||
| func TestRuleMatches(t *testing.T) { | ||||
| 	type requestToTest struct { | ||||
| 		request  authorizer.AttributesRecord | ||||
| 		expected bool | ||||
| 	} | ||||
| 	tests := []struct { | ||||
| 		name string | ||||
| 		rule rbacv1.PolicyRule | ||||
|  | ||||
| 		requestsToExpected map[authorizer.AttributesRecord]bool | ||||
| 		requestsToExpected []*requestToTest | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "star verb, exact match other", | ||||
| 			rule: rbacv1helpers.NewRule("*").Groups("group1").Resources("resource1").RuleOrDie(), | ||||
| 			requestsToExpected: map[authorizer.AttributesRecord]bool{ | ||||
| 				resourceRequest("verb1").Group("group1").Resource("resource1").New(): true, | ||||
| 				resourceRequest("verb1").Group("group2").Resource("resource1").New(): false, | ||||
| 				resourceRequest("verb1").Group("group1").Resource("resource2").New(): false, | ||||
| 				resourceRequest("verb1").Group("group2").Resource("resource2").New(): false, | ||||
| 				resourceRequest("verb2").Group("group1").Resource("resource1").New(): true, | ||||
| 				resourceRequest("verb2").Group("group2").Resource("resource1").New(): false, | ||||
| 				resourceRequest("verb2").Group("group1").Resource("resource2").New(): false, | ||||
| 				resourceRequest("verb2").Group("group2").Resource("resource2").New(): false, | ||||
| 			requestsToExpected: []*requestToTest{ | ||||
| 				{resourceRequest("verb1").Group("group1").Resource("resource1").New(), true}, | ||||
| 				{resourceRequest("verb1").Group("group2").Resource("resource1").New(), false}, | ||||
| 				{resourceRequest("verb1").Group("group1").Resource("resource2").New(), false}, | ||||
| 				{resourceRequest("verb1").Group("group2").Resource("resource2").New(), false}, | ||||
| 				{resourceRequest("verb2").Group("group1").Resource("resource1").New(), true}, | ||||
| 				{resourceRequest("verb2").Group("group2").Resource("resource1").New(), false}, | ||||
| 				{resourceRequest("verb2").Group("group1").Resource("resource2").New(), false}, | ||||
| 				{resourceRequest("verb2").Group("group2").Resource("resource2").New(), false}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "star group, exact match other", | ||||
| 			rule: rbacv1helpers.NewRule("verb1").Groups("*").Resources("resource1").RuleOrDie(), | ||||
| 			requestsToExpected: map[authorizer.AttributesRecord]bool{ | ||||
| 				resourceRequest("verb1").Group("group1").Resource("resource1").New(): true, | ||||
| 				resourceRequest("verb1").Group("group2").Resource("resource1").New(): true, | ||||
| 				resourceRequest("verb1").Group("group1").Resource("resource2").New(): false, | ||||
| 				resourceRequest("verb1").Group("group2").Resource("resource2").New(): false, | ||||
| 				resourceRequest("verb2").Group("group1").Resource("resource1").New(): false, | ||||
| 				resourceRequest("verb2").Group("group2").Resource("resource1").New(): false, | ||||
| 				resourceRequest("verb2").Group("group1").Resource("resource2").New(): false, | ||||
| 				resourceRequest("verb2").Group("group2").Resource("resource2").New(): false, | ||||
| 			requestsToExpected: []*requestToTest{ | ||||
| 				{resourceRequest("verb1").Group("group1").Resource("resource1").New(), true}, | ||||
| 				{resourceRequest("verb1").Group("group2").Resource("resource1").New(), true}, | ||||
| 				{resourceRequest("verb1").Group("group1").Resource("resource2").New(), false}, | ||||
| 				{resourceRequest("verb1").Group("group2").Resource("resource2").New(), false}, | ||||
| 				{resourceRequest("verb2").Group("group1").Resource("resource1").New(), false}, | ||||
| 				{resourceRequest("verb2").Group("group2").Resource("resource1").New(), false}, | ||||
| 				{resourceRequest("verb2").Group("group1").Resource("resource2").New(), false}, | ||||
| 				{resourceRequest("verb2").Group("group2").Resource("resource2").New(), false}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "star resource, exact match other", | ||||
| 			rule: rbacv1helpers.NewRule("verb1").Groups("group1").Resources("*").RuleOrDie(), | ||||
| 			requestsToExpected: map[authorizer.AttributesRecord]bool{ | ||||
| 				resourceRequest("verb1").Group("group1").Resource("resource1").New(): true, | ||||
| 				resourceRequest("verb1").Group("group2").Resource("resource1").New(): false, | ||||
| 				resourceRequest("verb1").Group("group1").Resource("resource2").New(): true, | ||||
| 				resourceRequest("verb1").Group("group2").Resource("resource2").New(): false, | ||||
| 				resourceRequest("verb2").Group("group1").Resource("resource1").New(): false, | ||||
| 				resourceRequest("verb2").Group("group2").Resource("resource1").New(): false, | ||||
| 				resourceRequest("verb2").Group("group1").Resource("resource2").New(): false, | ||||
| 				resourceRequest("verb2").Group("group2").Resource("resource2").New(): false, | ||||
| 			requestsToExpected: []*requestToTest{ | ||||
| 				{resourceRequest("verb1").Group("group1").Resource("resource1").New(), true}, | ||||
| 				{resourceRequest("verb1").Group("group2").Resource("resource1").New(), false}, | ||||
| 				{resourceRequest("verb1").Group("group1").Resource("resource2").New(), true}, | ||||
| 				{resourceRequest("verb1").Group("group2").Resource("resource2").New(), false}, | ||||
| 				{resourceRequest("verb2").Group("group1").Resource("resource1").New(), false}, | ||||
| 				{resourceRequest("verb2").Group("group2").Resource("resource1").New(), false}, | ||||
| 				{resourceRequest("verb2").Group("group1").Resource("resource2").New(), false}, | ||||
| 				{resourceRequest("verb2").Group("group2").Resource("resource2").New(), false}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "tuple expansion", | ||||
| 			rule: rbacv1helpers.NewRule("verb1", "verb2").Groups("group1", "group2").Resources("resource1", "resource2").RuleOrDie(), | ||||
| 			requestsToExpected: map[authorizer.AttributesRecord]bool{ | ||||
| 				resourceRequest("verb1").Group("group1").Resource("resource1").New(): true, | ||||
| 				resourceRequest("verb1").Group("group2").Resource("resource1").New(): true, | ||||
| 				resourceRequest("verb1").Group("group1").Resource("resource2").New(): true, | ||||
| 				resourceRequest("verb1").Group("group2").Resource("resource2").New(): true, | ||||
| 				resourceRequest("verb2").Group("group1").Resource("resource1").New(): true, | ||||
| 				resourceRequest("verb2").Group("group2").Resource("resource1").New(): true, | ||||
| 				resourceRequest("verb2").Group("group1").Resource("resource2").New(): true, | ||||
| 				resourceRequest("verb2").Group("group2").Resource("resource2").New(): true, | ||||
| 			requestsToExpected: []*requestToTest{ | ||||
| 				{resourceRequest("verb1").Group("group1").Resource("resource1").New(), true}, | ||||
| 				{resourceRequest("verb1").Group("group2").Resource("resource1").New(), true}, | ||||
| 				{resourceRequest("verb1").Group("group1").Resource("resource2").New(), true}, | ||||
| 				{resourceRequest("verb1").Group("group2").Resource("resource2").New(), true}, | ||||
| 				{resourceRequest("verb2").Group("group1").Resource("resource1").New(), true}, | ||||
| 				{resourceRequest("verb2").Group("group2").Resource("resource1").New(), true}, | ||||
| 				{resourceRequest("verb2").Group("group1").Resource("resource2").New(), true}, | ||||
| 				{resourceRequest("verb2").Group("group2").Resource("resource2").New(), true}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "subresource expansion", | ||||
| 			rule: rbacv1helpers.NewRule("*").Groups("*").Resources("resource1/subresource1").RuleOrDie(), | ||||
| 			requestsToExpected: map[authorizer.AttributesRecord]bool{ | ||||
| 				resourceRequest("verb1").Group("group1").Resource("resource1").Subresource("subresource1").New(): true, | ||||
| 				resourceRequest("verb1").Group("group2").Resource("resource1").Subresource("subresource2").New(): false, | ||||
| 				resourceRequest("verb1").Group("group1").Resource("resource2").Subresource("subresource1").New(): false, | ||||
| 				resourceRequest("verb1").Group("group2").Resource("resource2").Subresource("subresource1").New(): false, | ||||
| 				resourceRequest("verb2").Group("group1").Resource("resource1").Subresource("subresource1").New(): true, | ||||
| 				resourceRequest("verb2").Group("group2").Resource("resource1").Subresource("subresource2").New(): false, | ||||
| 				resourceRequest("verb2").Group("group1").Resource("resource2").Subresource("subresource1").New(): false, | ||||
| 				resourceRequest("verb2").Group("group2").Resource("resource2").Subresource("subresource1").New(): false, | ||||
| 			requestsToExpected: []*requestToTest{ | ||||
| 				{resourceRequest("verb1").Group("group1").Resource("resource1").Subresource("subresource1").New(), true}, | ||||
| 				{resourceRequest("verb1").Group("group2").Resource("resource1").Subresource("subresource2").New(), false}, | ||||
| 				{resourceRequest("verb1").Group("group1").Resource("resource2").Subresource("subresource1").New(), false}, | ||||
| 				{resourceRequest("verb1").Group("group2").Resource("resource2").Subresource("subresource1").New(), false}, | ||||
| 				{resourceRequest("verb2").Group("group1").Resource("resource1").Subresource("subresource1").New(), true}, | ||||
| 				{resourceRequest("verb2").Group("group2").Resource("resource1").Subresource("subresource2").New(), false}, | ||||
| 				{resourceRequest("verb2").Group("group1").Resource("resource2").Subresource("subresource1").New(), false}, | ||||
| 				{resourceRequest("verb2").Group("group2").Resource("resource2").Subresource("subresource1").New(), false}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "star nonresource, exact match other", | ||||
| 			rule: rbacv1helpers.NewRule("verb1").URLs("*").RuleOrDie(), | ||||
| 			requestsToExpected: map[authorizer.AttributesRecord]bool{ | ||||
| 				nonresourceRequest("verb1").URL("/foo").New():         true, | ||||
| 				nonresourceRequest("verb1").URL("/foo/bar").New():     true, | ||||
| 				nonresourceRequest("verb1").URL("/foo/baz").New():     true, | ||||
| 				nonresourceRequest("verb1").URL("/foo/bar/one").New(): true, | ||||
| 				nonresourceRequest("verb1").URL("/foo/baz/one").New(): true, | ||||
| 				nonresourceRequest("verb2").URL("/foo").New():         false, | ||||
| 				nonresourceRequest("verb2").URL("/foo/bar").New():     false, | ||||
| 				nonresourceRequest("verb2").URL("/foo/baz").New():     false, | ||||
| 				nonresourceRequest("verb2").URL("/foo/bar/one").New(): false, | ||||
| 				nonresourceRequest("verb2").URL("/foo/baz/one").New(): false, | ||||
| 			requestsToExpected: []*requestToTest{ | ||||
| 				{nonresourceRequest("verb1").URL("/foo").New(), true}, | ||||
| 				{nonresourceRequest("verb1").URL("/foo/bar").New(), true}, | ||||
| 				{nonresourceRequest("verb1").URL("/foo/baz").New(), true}, | ||||
| 				{nonresourceRequest("verb1").URL("/foo/bar/one").New(), true}, | ||||
| 				{nonresourceRequest("verb1").URL("/foo/baz/one").New(), true}, | ||||
| 				{nonresourceRequest("verb2").URL("/foo").New(), false}, | ||||
| 				{nonresourceRequest("verb2").URL("/foo/bar").New(), false}, | ||||
| 				{nonresourceRequest("verb2").URL("/foo/baz").New(), false}, | ||||
| 				{nonresourceRequest("verb2").URL("/foo/bar/one").New(), false}, | ||||
| 				{nonresourceRequest("verb2").URL("/foo/baz/one").New(), false}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "star nonresource subpath", | ||||
| 			rule: rbacv1helpers.NewRule("verb1").URLs("/foo/*").RuleOrDie(), | ||||
| 			requestsToExpected: map[authorizer.AttributesRecord]bool{ | ||||
| 				nonresourceRequest("verb1").URL("/foo").New():            false, | ||||
| 				nonresourceRequest("verb1").URL("/foo/bar").New():        true, | ||||
| 				nonresourceRequest("verb1").URL("/foo/baz").New():        true, | ||||
| 				nonresourceRequest("verb1").URL("/foo/bar/one").New():    true, | ||||
| 				nonresourceRequest("verb1").URL("/foo/baz/one").New():    true, | ||||
| 				nonresourceRequest("verb1").URL("/notfoo").New():         false, | ||||
| 				nonresourceRequest("verb1").URL("/notfoo/bar").New():     false, | ||||
| 				nonresourceRequest("verb1").URL("/notfoo/baz").New():     false, | ||||
| 				nonresourceRequest("verb1").URL("/notfoo/bar/one").New(): false, | ||||
| 				nonresourceRequest("verb1").URL("/notfoo/baz/one").New(): false, | ||||
| 			requestsToExpected: []*requestToTest{ | ||||
| 				{nonresourceRequest("verb1").URL("/foo").New(), false}, | ||||
| 				{nonresourceRequest("verb1").URL("/foo/bar").New(), true}, | ||||
| 				{nonresourceRequest("verb1").URL("/foo/baz").New(), true}, | ||||
| 				{nonresourceRequest("verb1").URL("/foo/bar/one").New(), true}, | ||||
| 				{nonresourceRequest("verb1").URL("/foo/baz/one").New(), true}, | ||||
| 				{nonresourceRequest("verb1").URL("/notfoo").New(), false}, | ||||
| 				{nonresourceRequest("verb1").URL("/notfoo/bar").New(), false}, | ||||
| 				{nonresourceRequest("verb1").URL("/notfoo/baz").New(), false}, | ||||
| 				{nonresourceRequest("verb1").URL("/notfoo/bar/one").New(), false}, | ||||
| 				{nonresourceRequest("verb1").URL("/notfoo/baz/one").New(), false}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "star verb, exact nonresource", | ||||
| 			rule: rbacv1helpers.NewRule("*").URLs("/foo", "/foo/bar/one").RuleOrDie(), | ||||
| 			requestsToExpected: map[authorizer.AttributesRecord]bool{ | ||||
| 				nonresourceRequest("verb1").URL("/foo").New():         true, | ||||
| 				nonresourceRequest("verb1").URL("/foo/bar").New():     false, | ||||
| 				nonresourceRequest("verb1").URL("/foo/baz").New():     false, | ||||
| 				nonresourceRequest("verb1").URL("/foo/bar/one").New(): true, | ||||
| 				nonresourceRequest("verb1").URL("/foo/baz/one").New(): false, | ||||
| 				nonresourceRequest("verb2").URL("/foo").New():         true, | ||||
| 				nonresourceRequest("verb2").URL("/foo/bar").New():     false, | ||||
| 				nonresourceRequest("verb2").URL("/foo/baz").New():     false, | ||||
| 				nonresourceRequest("verb2").URL("/foo/bar/one").New(): true, | ||||
| 				nonresourceRequest("verb2").URL("/foo/baz/one").New(): false, | ||||
| 			requestsToExpected: []*requestToTest{ | ||||
| 				{nonresourceRequest("verb1").URL("/foo").New(), true}, | ||||
| 				{nonresourceRequest("verb1").URL("/foo/bar").New(), false}, | ||||
| 				{nonresourceRequest("verb1").URL("/foo/baz").New(), false}, | ||||
| 				{nonresourceRequest("verb1").URL("/foo/bar/one").New(), true}, | ||||
| 				{nonresourceRequest("verb1").URL("/foo/baz/one").New(), false}, | ||||
| 				{nonresourceRequest("verb2").URL("/foo").New(), true}, | ||||
| 				{nonresourceRequest("verb2").URL("/foo/bar").New(), false}, | ||||
| 				{nonresourceRequest("verb2").URL("/foo/baz").New(), false}, | ||||
| 				{nonresourceRequest("verb2").URL("/foo/bar/one").New(), true}, | ||||
| 				{nonresourceRequest("verb2").URL("/foo/baz/one").New(), false}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tc := range tests { | ||||
| 		for request, expected := range tc.requestsToExpected { | ||||
| 			if e, a := expected, RuleAllows(request, &tc.rule); e != a { | ||||
| 				t.Errorf("%q: expected %v, got %v for %v", tc.name, e, a, request) | ||||
| 		for _, requestToTest := range tc.requestsToExpected { | ||||
| 			if e, a := requestToTest.expected, RuleAllows(requestToTest.request, &tc.rule); e != a { | ||||
| 				t.Errorf("%q: expected %v, got %v for %v", tc.name, e, a, requestToTest.request) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -32,6 +32,10 @@ import ( | ||||
| type LabelSelectorValidationOptions struct { | ||||
| 	// Allow invalid label value in selector | ||||
| 	AllowInvalidLabelValueInSelector bool | ||||
|  | ||||
| 	// Allows an operator that is not interpretable to pass validation.  This is useful for cases where a broader check | ||||
| 	// can be performed, as in a *SubjectAccessReview | ||||
| 	AllowUnknownOperatorInRequirement bool | ||||
| } | ||||
|  | ||||
| // LabelSelectorHasInvalidLabelValue returns true if the given selector contains an invalid label value in a match expression. | ||||
| @@ -79,8 +83,10 @@ func ValidateLabelSelectorRequirement(sr metav1.LabelSelectorRequirement, opts L | ||||
| 			allErrs = append(allErrs, field.Forbidden(fldPath.Child("values"), "may not be specified when `operator` is 'Exists' or 'DoesNotExist'")) | ||||
| 		} | ||||
| 	default: | ||||
| 		if !opts.AllowUnknownOperatorInRequirement { | ||||
| 			allErrs = append(allErrs, field.Invalid(fldPath.Child("operator"), sr.Operator, "not a valid selector operator")) | ||||
| 		} | ||||
| 	} | ||||
| 	allErrs = append(allErrs, ValidateLabelName(sr.Key, fldPath.Child("key"))...) | ||||
| 	if !opts.AllowInvalidLabelValueInSelector { | ||||
| 		for valueIndex, value := range sr.Values { | ||||
| @@ -113,6 +119,39 @@ func ValidateLabels(labels map[string]string, fldPath *field.Path) field.ErrorLi | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
| // FieldSelectorValidationOptions is a struct that can be passed to ValidateFieldSelectorRequirement to record the validate options | ||||
| type FieldSelectorValidationOptions struct { | ||||
| 	// Allows an operator that is not interpretable to pass validation.  This is useful for cases where a broader check | ||||
| 	// can be performed, as in a *SubjectAccessReview | ||||
| 	AllowUnknownOperatorInRequirement bool | ||||
| } | ||||
|  | ||||
| // ValidateLabelSelectorRequirement validates the requirement according to the opts and returns any validation errors. | ||||
| func ValidateFieldSelectorRequirement(requirement metav1.FieldSelectorRequirement, opts FieldSelectorValidationOptions, fldPath *field.Path) field.ErrorList { | ||||
| 	allErrs := field.ErrorList{} | ||||
|  | ||||
| 	if len(requirement.Key) == 0 { | ||||
| 		allErrs = append(allErrs, field.Required(fldPath.Child("key"), "must be specified")) | ||||
| 	} | ||||
|  | ||||
| 	switch requirement.Operator { | ||||
| 	case metav1.FieldSelectorOpIn, metav1.FieldSelectorOpNotIn: | ||||
| 		if len(requirement.Values) == 0 { | ||||
| 			allErrs = append(allErrs, field.Required(fldPath.Child("values"), "must be specified when `operator` is 'In' or 'NotIn'")) | ||||
| 		} | ||||
| 	case metav1.FieldSelectorOpExists, metav1.FieldSelectorOpDoesNotExist: | ||||
| 		if len(requirement.Values) > 0 { | ||||
| 			allErrs = append(allErrs, field.Forbidden(fldPath.Child("values"), "may not be specified when `operator` is 'Exists' or 'DoesNotExist'")) | ||||
| 		} | ||||
| 	default: | ||||
| 		if !opts.AllowUnknownOperatorInRequirement { | ||||
| 			allErrs = append(allErrs, field.Invalid(fldPath.Child("operator"), requirement.Operator, "not a valid selector operator")) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
| func ValidateDeleteOptions(options *metav1.DeleteOptions) field.ErrorList { | ||||
| 	allErrs := field.ErrorList{} | ||||
| 	//lint:file-ignore SA1019 Keep validation for deprecated OrphanDependents option until it's being removed | ||||
|   | ||||
| @@ -470,7 +470,7 @@ func TestLabelSelectorMatchExpression(t *testing.T) { | ||||
| 	}} | ||||
| 	for index, testCase := range testCases { | ||||
| 		t.Run(testCase.name, func(t *testing.T) { | ||||
| 			allErrs := ValidateLabelSelector(testCase.labelSelector, LabelSelectorValidationOptions{false}, field.NewPath("labelSelector")) | ||||
| 			allErrs := ValidateLabelSelector(testCase.labelSelector, LabelSelectorValidationOptions{AllowInvalidLabelValueInSelector: false}, field.NewPath("labelSelector")) | ||||
| 			if len(allErrs) != testCase.wantErrorNumber { | ||||
| 				t.Errorf("case[%d]: expected failure", index) | ||||
| 			} | ||||
|   | ||||
| @@ -45,6 +45,19 @@ var ( | ||||
| // Requirements is AND of all requirements. | ||||
| type Requirements []Requirement | ||||
|  | ||||
| func (r Requirements) String() string { | ||||
| 	var sb strings.Builder | ||||
|  | ||||
| 	for i, requirement := range r { | ||||
| 		if i > 0 { | ||||
| 			sb.WriteString(", ") | ||||
| 		} | ||||
| 		sb.WriteString(requirement.String()) | ||||
| 	} | ||||
|  | ||||
| 	return sb.String() | ||||
| } | ||||
|  | ||||
| // Selector represents a label selector. | ||||
| type Selector interface { | ||||
| 	// Matches returns true if this selector matches the given set of labels. | ||||
| @@ -285,6 +298,13 @@ func (r *Requirement) Values() sets.String { | ||||
| 	return ret | ||||
| } | ||||
|  | ||||
| // ValuesUnsorted returns a copy of requirement values as passed to NewRequirement without sorting. | ||||
| func (r *Requirement) ValuesUnsorted() []string { | ||||
| 	ret := make([]string, 0, len(r.strValues)) | ||||
| 	ret = append(ret, r.strValues...) | ||||
| 	return ret | ||||
| } | ||||
|  | ||||
| // Equal checks the equality of requirement. | ||||
| func (r Requirement) Equal(x Requirement) bool { | ||||
| 	if r.key != x.key { | ||||
|   | ||||
| @@ -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,7 +76,13 @@ 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{ | ||||
| 	type SerializableAttributes struct { | ||||
| 		authorizer.AttributesRecord | ||||
| 		LabelSelector string | ||||
| 	} | ||||
|  | ||||
| 	serializableAttributes := SerializableAttributes{ | ||||
| 		AttributesRecord: authorizer.AttributesRecord{ | ||||
| 			Verb:            a.GetVerb(), | ||||
| 			Namespace:       a.GetNamespace(), | ||||
| 			APIGroup:        a.GetAPIGroup(), | ||||
| @@ -82,6 +92,15 @@ func (ca *cachingAuthorizer) Authorize(ctx context.Context, a authorizer.Attribu | ||||
| 			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 { | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 ( | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| 		EnableAuthorizationSelector bool | ||||
| 	}{ | ||||
| 		"non-resource root": { | ||||
| 			Verb: "POST", | ||||
| @@ -102,9 +114,104 @@ 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: <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 { | ||||
| 		t.Run(k, func(t *testing.T) { | ||||
| 			if tc.EnableAuthorizationSelector { | ||||
| 				featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true) | ||||
| 			} | ||||
|  | ||||
| 			req, _ := http.NewRequest(tc.Verb, tc.Path, nil) | ||||
| 			req.RemoteAddr = "127.0.0.1" | ||||
|  | ||||
| @@ -122,6 +229,7 @@ func TestGetAuthorizerAttributes(t *testing.T) { | ||||
| 			} else if !reflect.DeepEqual(attribs, tc.ExpectedAttributes) { | ||||
| 				t.Errorf("%s: expected\n\t%#v\ngot\n\t%#v", k, tc.ExpectedAttributes, attribs) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| 		method                string | ||||
| 		url                   string | ||||
| 		expectedName          string | ||||
| 		expectedErr           error | ||||
| 		expectedVerb          string | ||||
| 		expectedFieldSelector string | ||||
| 		expectedLabelSelector string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:                  "no selector", | ||||
| 			method:                "GET", | ||||
| 			url:                   "/apis/group/version/resource", | ||||
| 			expectedVerb:          "list", | ||||
| 			expectedFieldSelector: "", | ||||
| 		}, | ||||
| 		{ | ||||
| 			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", | ||||
| 			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", | ||||
| 			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", | ||||
| 			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", | ||||
| 			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", | ||||
| 			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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -88,6 +88,8 @@ func v1beta1ResourceAttributesToV1ResourceAttributes(in *authorizationv1beta1.Re | ||||
| 		Resource:      in.Resource, | ||||
| 		Subresource:   in.Subresource, | ||||
| 		Name:          in.Name, | ||||
| 		FieldSelector: in.FieldSelector, | ||||
| 		LabelSelector: in.LabelSelector, | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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 ( | ||||
| @@ -482,6 +578,8 @@ func v1ResourceAttributesToV1beta1ResourceAttributes(in *authorizationv1.Resourc | ||||
| 		Resource:      in.Resource, | ||||
| 		Subresource:   in.Subresource, | ||||
| 		Name:          in.Name, | ||||
| 		FieldSelector: in.FieldSelector, | ||||
| 		LabelSelector: in.LabelSelector, | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user