add field and label selectors to authorization attributes

Co-authored-by: Jordan Liggitt <liggitt@google.com>
This commit is contained in:
David Eads 2024-05-23 15:12:26 -04:00 committed by Jordan Liggitt
parent f5e5bef2e0
commit 92e3445e9d
No known key found for this signature in database
25 changed files with 2388 additions and 226 deletions

View File

@ -17,8 +17,11 @@ limitations under the License.
package validation package validation
import ( import (
"fmt"
apiequality "k8s.io/apimachinery/pkg/api/equality" apiequality "k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization" 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 { 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, 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 return allErrs
} }
@ -50,6 +54,7 @@ func ValidateSelfSubjectAccessReviewSpec(spec authorizationapi.SelfSubjectAccess
if spec.ResourceAttributes == nil && spec.NonResourceAttributes == nil { 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, 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 return allErrs
} }
@ -99,3 +104,59 @@ func ValidateLocalSubjectAccessReview(sar *authorizationapi.LocalSubjectAccessRe
return allErrs 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
}

View File

@ -29,6 +29,50 @@ func TestValidateSARSpec(t *testing.T) {
successCases := []authorizationapi.SubjectAccessReviewSpec{ successCases := []authorizationapi.SubjectAccessReviewSpec{
{ResourceAttributes: &authorizationapi.ResourceAttributes{}, User: "me"}, {ResourceAttributes: &authorizationapi.ResourceAttributes{}, User: "me"},
{NonResourceAttributes: &authorizationapi.NonResourceAttributes{}, Groups: []string{"my-group"}}, {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 { for _, successCase := range successCases {
if errs := ValidateSubjectAccessReviewSpec(successCase, field.NewPath("spec")); len(errs) != 0 { if errs := ValidateSubjectAccessReviewSpec(successCase, field.NewPath("spec")); len(errs) != 0 {
@ -58,9 +102,237 @@ func TestValidateSARSpec(t *testing.T) {
ResourceAttributes: &authorizationapi.ResourceAttributes{}, ResourceAttributes: &authorizationapi.ResourceAttributes{},
}, },
msg: `spec.user: Invalid value: "": at least one of user or group must be specified`, 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 { for _, c := range errorCases {
t.Run(c.name, func(t *testing.T) {
errs := ValidateSubjectAccessReviewSpec(c.obj, field.NewPath("spec")) errs := ValidateSubjectAccessReviewSpec(c.obj, field.NewPath("spec"))
if len(errs) == 0 { if len(errs) == 0 {
t.Errorf("%s: expected failure for %q", c.name, c.msg) 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) { } else if !strings.Contains(errs[0].Error(), c.msg) {
t.Errorf("%s: unexpected error: %q, expected: %q", c.name, errs[0], 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{}, NonResourceAttributes: &authorizationapi.NonResourceAttributes{},
}, },
msg: "cannot be specified in combination with resourceAttributes", 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 { for _, c := range errorCases {
@ -175,6 +461,23 @@ func TestValidateLocalSAR(t *testing.T) {
}, },
}, },
msg: "disallowed on this kind of request", 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 { for _, c := range errorCases {

View File

@ -64,6 +64,8 @@ var typesAllowedTags = map[reflect.Type]bool{
reflect.TypeOf(metav1.ObjectMeta{}): true, reflect.TypeOf(metav1.ObjectMeta{}): true,
reflect.TypeOf(metav1.OwnerReference{}): true, reflect.TypeOf(metav1.OwnerReference{}): true,
reflect.TypeOf(metav1.LabelSelector{}): true, reflect.TypeOf(metav1.LabelSelector{}): true,
reflect.TypeOf(metav1.LabelSelectorRequirement{}): true,
reflect.TypeOf(metav1.FieldSelectorRequirement{}): true,
reflect.TypeOf(metav1.GetOptions{}): true, reflect.TypeOf(metav1.GetOptions{}): true,
reflect.TypeOf(metav1.ListOptions{}): true, reflect.TypeOf(metav1.ListOptions{}): true,
reflect.TypeOf(metav1.DeleteOptions{}): true, reflect.TypeOf(metav1.DeleteOptions{}): true,

View File

@ -1253,6 +1253,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
genericfeatures.APIServingWithRoutine: {Default: true, PreRelease: featuregate.Beta}, genericfeatures.APIServingWithRoutine: {Default: true, PreRelease: featuregate.Beta},
genericfeatures.AuthorizeWithSelectors: {Default: false, PreRelease: featuregate.Alpha},
genericfeatures.ConsistentListFromCache: {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 genericfeatures.CustomResourceValidationExpressions: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.31

View File

@ -25,7 +25,9 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request" genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
genericfeatures "k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
utilfeature "k8s.io/apiserver/pkg/util/feature"
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization" authorizationapi "k8s.io/kubernetes/pkg/apis/authorization"
authorizationvalidation "k8s.io/kubernetes/pkg/apis/authorization/validation" authorizationvalidation "k8s.io/kubernetes/pkg/apis/authorization/validation"
authorizationutil "k8s.io/kubernetes/pkg/registry/authorization/util" 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 { if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("not a LocaLocalSubjectAccessReview: %#v", obj)) 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 { if errs := authorizationvalidation.ValidateLocalSubjectAccessReview(localSubjectAccessReview); len(errs) > 0 {
return nil, apierrors.NewInvalid(authorizationapi.Kind(localSubjectAccessReview.Kind), "", errs) 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), Denied: (decision == authorizer.DecisionDeny),
Reason: reason, Reason: reason,
} }
if evaluationErr != nil { localSubjectAccessReview.Status.EvaluationError = authorizationutil.BuildEvaluationError(evaluationErr, authorizationAttributes)
localSubjectAccessReview.Status.EvaluationError = evaluationErr.Error()
}
return localSubjectAccessReview, nil return localSubjectAccessReview, nil
} }

View File

@ -25,7 +25,9 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request" genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
genericfeatures "k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
utilfeature "k8s.io/apiserver/pkg/util/feature"
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization" authorizationapi "k8s.io/kubernetes/pkg/apis/authorization"
authorizationvalidation "k8s.io/kubernetes/pkg/apis/authorization/validation" authorizationvalidation "k8s.io/kubernetes/pkg/apis/authorization/validation"
authorizationutil "k8s.io/kubernetes/pkg/registry/authorization/util" 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 { if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("not a SelfSubjectAccessReview: %#v", obj)) 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 { if errs := authorizationvalidation.ValidateSelfSubjectAccessReview(selfSAR); len(errs) > 0 {
return nil, apierrors.NewInvalid(authorizationapi.Kind(selfSAR.Kind), "", errs) 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), Denied: (decision == authorizer.DecisionDeny),
Reason: reason, Reason: reason,
} }
if evaluationErr != nil { selfSAR.Status.EvaluationError = authorizationutil.BuildEvaluationError(evaluationErr, authorizationAttributes)
selfSAR.Status.EvaluationError = evaluationErr.Error()
}
return selfSAR, nil return selfSAR, nil
} }

View File

@ -24,7 +24,9 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
genericfeatures "k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
utilfeature "k8s.io/apiserver/pkg/util/feature"
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization" authorizationapi "k8s.io/kubernetes/pkg/apis/authorization"
authorizationvalidation "k8s.io/kubernetes/pkg/apis/authorization/validation" authorizationvalidation "k8s.io/kubernetes/pkg/apis/authorization/validation"
authorizationutil "k8s.io/kubernetes/pkg/registry/authorization/util" 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 { if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("not a SubjectAccessReview: %#v", obj)) 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 { if errs := authorizationvalidation.ValidateSubjectAccessReview(subjectAccessReview); len(errs) > 0 {
return nil, apierrors.NewInvalid(authorizationapi.Kind(subjectAccessReview.Kind), "", errs) 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), Denied: (decision == authorizer.DecisionDeny),
Reason: reason, Reason: reason,
} }
if evaluationErr != nil { subjectAccessReview.Status.EvaluationError = authorizationutil.BuildEvaluationError(evaluationErr, authorizationAttributes)
subjectAccessReview.Status.EvaluationError = evaluationErr.Error()
}
return subjectAccessReview, nil return subjectAccessReview, nil
} }

View File

@ -24,10 +24,15 @@ import (
"testing" "testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/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/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request" genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
genericfeatures "k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/registry/rest" "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" authorizationapi "k8s.io/kubernetes/pkg/apis/authorization"
) )
@ -187,8 +192,52 @@ func TestCreate(t *testing.T) {
Denied: true, 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 { for k, tc := range testcases {
auth := &fakeAuthorizer{ auth := &fakeAuthorizer{
decision: tc.decision, decision: tc.decision,
@ -208,8 +257,13 @@ func TestCreate(t *testing.T) {
} }
continue continue
} }
if !reflect.DeepEqual(auth.attrs, tc.expectedAttrs) { gotAttrs := auth.attrs.(authorizer.AttributesRecord)
t.Errorf("%s: expected\n%#v\ngot\n%#v", k, tc.expectedAttrs, auth.attrs) 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 status := result.(*authorizationapi.SubjectAccessReview).Status
if !reflect.DeepEqual(status, tc.expectedStatus) { 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
}

View File

@ -17,14 +17,24 @@ limitations under the License.
package util package util
import ( 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/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization" 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 // 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 { func ResourceAttributesFrom(user user.Info, in authorizationapi.ResourceAttributes) authorizer.AttributesRecord {
return authorizer.AttributesRecord{ ret := authorizer.AttributesRecord{
User: user, User: user,
Verb: in.Verb, Verb: in.Verb,
Namespace: in.Namespace, Namespace: in.Namespace,
@ -35,6 +45,129 @@ func ResourceAttributesFrom(user user.Info, in authorizationapi.ResourceAttribut
Name: in.Name, Name: in.Name,
ResourceRequest: true, 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 // 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 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, "; ")
}

View File

@ -17,12 +17,21 @@ limitations under the License.
package util package util
import ( import (
"errors"
"reflect" "reflect"
"testing" "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/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization" authorizationapi "k8s.io/kubernetes/pkg/apis/authorization"
) )
@ -37,6 +46,10 @@ func TestResourceAttributesFrom(t *testing.T) {
"Subresource", "Subresource",
"Name", "Name",
// Fields we read and parse in ResourceAttributesFrom
"FieldSelector",
"LabelSelector",
// Fields we copy in NonResourceAttributesFrom // Fields we copy in NonResourceAttributesFrom
"Path", "Path",
"Verb", "Verb",
@ -60,6 +73,12 @@ func TestResourceAttributesFrom(t *testing.T) {
"Name", "Name",
"ResourceRequest", "ResourceRequest",
// Fields we compute and set in ResourceAttributesFrom
"FieldSelectorRequirements",
"FieldSelectorParsingErr",
"LabelSelectorRequirements",
"LabelSelectorParsingErr",
// Fields we set in NonResourceAttributesFrom // Fields we set in NonResourceAttributesFrom
"User", "User",
"ResourceRequest", "ResourceRequest",
@ -75,6 +94,14 @@ func TestResourceAttributesFrom(t *testing.T) {
} }
func TestAuthorizationAttributesFrom(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 { type args struct {
spec authorizationapi.SubjectAccessReviewSpec spec authorizationapi.SubjectAccessReviewSpec
} }
@ -82,6 +109,7 @@ func TestAuthorizationAttributesFrom(t *testing.T) {
name string name string
args args args args
want authorizer.AttributesRecord want authorizer.AttributesRecord
enableAuthorizationSelector bool
}{ }{
{ {
name: "nonresource", name: "nonresource",
@ -162,11 +190,461 @@ func TestAuthorizationAttributesFrom(t *testing.T) {
ResourceRequest: true, 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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) { 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)
} }
}) })
} }

View File

@ -24,6 +24,8 @@ import (
rbacv1 "k8s.io/api/rbac/v1" rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/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/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
rbacv1helpers "k8s.io/kubernetes/pkg/apis/rbac/v1" 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) GetAPIVersion() string { return "" }
func (d *defaultAttributes) IsResourceRequest() bool { return true } func (d *defaultAttributes) IsResourceRequest() bool { return true }
func (d *defaultAttributes) GetPath() string { return "" } 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) { func TestAuthorizer(t *testing.T) {
tests := []struct { tests := []struct {
@ -263,135 +271,139 @@ func TestAuthorizer(t *testing.T) {
} }
func TestRuleMatches(t *testing.T) { func TestRuleMatches(t *testing.T) {
type requestToTest struct {
request authorizer.AttributesRecord
expected bool
}
tests := []struct { tests := []struct {
name string name string
rule rbacv1.PolicyRule rule rbacv1.PolicyRule
requestsToExpected map[authorizer.AttributesRecord]bool requestsToExpected []*requestToTest
}{ }{
{ {
name: "star verb, exact match other", name: "star verb, exact match other",
rule: rbacv1helpers.NewRule("*").Groups("group1").Resources("resource1").RuleOrDie(), rule: rbacv1helpers.NewRule("*").Groups("group1").Resources("resource1").RuleOrDie(),
requestsToExpected: map[authorizer.AttributesRecord]bool{ requestsToExpected: []*requestToTest{
resourceRequest("verb1").Group("group1").Resource("resource1").New(): true, {resourceRequest("verb1").Group("group1").Resource("resource1").New(), true},
resourceRequest("verb1").Group("group2").Resource("resource1").New(): false, {resourceRequest("verb1").Group("group2").Resource("resource1").New(), false},
resourceRequest("verb1").Group("group1").Resource("resource2").New(): false, {resourceRequest("verb1").Group("group1").Resource("resource2").New(), false},
resourceRequest("verb1").Group("group2").Resource("resource2").New(): false, {resourceRequest("verb1").Group("group2").Resource("resource2").New(), false},
resourceRequest("verb2").Group("group1").Resource("resource1").New(): true, {resourceRequest("verb2").Group("group1").Resource("resource1").New(), true},
resourceRequest("verb2").Group("group2").Resource("resource1").New(): false, {resourceRequest("verb2").Group("group2").Resource("resource1").New(), false},
resourceRequest("verb2").Group("group1").Resource("resource2").New(): false, {resourceRequest("verb2").Group("group1").Resource("resource2").New(), false},
resourceRequest("verb2").Group("group2").Resource("resource2").New(): false, {resourceRequest("verb2").Group("group2").Resource("resource2").New(), false},
}, },
}, },
{ {
name: "star group, exact match other", name: "star group, exact match other",
rule: rbacv1helpers.NewRule("verb1").Groups("*").Resources("resource1").RuleOrDie(), rule: rbacv1helpers.NewRule("verb1").Groups("*").Resources("resource1").RuleOrDie(),
requestsToExpected: map[authorizer.AttributesRecord]bool{ requestsToExpected: []*requestToTest{
resourceRequest("verb1").Group("group1").Resource("resource1").New(): true, {resourceRequest("verb1").Group("group1").Resource("resource1").New(), true},
resourceRequest("verb1").Group("group2").Resource("resource1").New(): true, {resourceRequest("verb1").Group("group2").Resource("resource1").New(), true},
resourceRequest("verb1").Group("group1").Resource("resource2").New(): false, {resourceRequest("verb1").Group("group1").Resource("resource2").New(), false},
resourceRequest("verb1").Group("group2").Resource("resource2").New(): false, {resourceRequest("verb1").Group("group2").Resource("resource2").New(), false},
resourceRequest("verb2").Group("group1").Resource("resource1").New(): false, {resourceRequest("verb2").Group("group1").Resource("resource1").New(), false},
resourceRequest("verb2").Group("group2").Resource("resource1").New(): false, {resourceRequest("verb2").Group("group2").Resource("resource1").New(), false},
resourceRequest("verb2").Group("group1").Resource("resource2").New(): false, {resourceRequest("verb2").Group("group1").Resource("resource2").New(), false},
resourceRequest("verb2").Group("group2").Resource("resource2").New(): false, {resourceRequest("verb2").Group("group2").Resource("resource2").New(), false},
}, },
}, },
{ {
name: "star resource, exact match other", name: "star resource, exact match other",
rule: rbacv1helpers.NewRule("verb1").Groups("group1").Resources("*").RuleOrDie(), rule: rbacv1helpers.NewRule("verb1").Groups("group1").Resources("*").RuleOrDie(),
requestsToExpected: map[authorizer.AttributesRecord]bool{ requestsToExpected: []*requestToTest{
resourceRequest("verb1").Group("group1").Resource("resource1").New(): true, {resourceRequest("verb1").Group("group1").Resource("resource1").New(), true},
resourceRequest("verb1").Group("group2").Resource("resource1").New(): false, {resourceRequest("verb1").Group("group2").Resource("resource1").New(), false},
resourceRequest("verb1").Group("group1").Resource("resource2").New(): true, {resourceRequest("verb1").Group("group1").Resource("resource2").New(), true},
resourceRequest("verb1").Group("group2").Resource("resource2").New(): false, {resourceRequest("verb1").Group("group2").Resource("resource2").New(), false},
resourceRequest("verb2").Group("group1").Resource("resource1").New(): false, {resourceRequest("verb2").Group("group1").Resource("resource1").New(), false},
resourceRequest("verb2").Group("group2").Resource("resource1").New(): false, {resourceRequest("verb2").Group("group2").Resource("resource1").New(), false},
resourceRequest("verb2").Group("group1").Resource("resource2").New(): false, {resourceRequest("verb2").Group("group1").Resource("resource2").New(), false},
resourceRequest("verb2").Group("group2").Resource("resource2").New(): false, {resourceRequest("verb2").Group("group2").Resource("resource2").New(), false},
}, },
}, },
{ {
name: "tuple expansion", name: "tuple expansion",
rule: rbacv1helpers.NewRule("verb1", "verb2").Groups("group1", "group2").Resources("resource1", "resource2").RuleOrDie(), rule: rbacv1helpers.NewRule("verb1", "verb2").Groups("group1", "group2").Resources("resource1", "resource2").RuleOrDie(),
requestsToExpected: map[authorizer.AttributesRecord]bool{ requestsToExpected: []*requestToTest{
resourceRequest("verb1").Group("group1").Resource("resource1").New(): true, {resourceRequest("verb1").Group("group1").Resource("resource1").New(), true},
resourceRequest("verb1").Group("group2").Resource("resource1").New(): true, {resourceRequest("verb1").Group("group2").Resource("resource1").New(), true},
resourceRequest("verb1").Group("group1").Resource("resource2").New(): true, {resourceRequest("verb1").Group("group1").Resource("resource2").New(), true},
resourceRequest("verb1").Group("group2").Resource("resource2").New(): true, {resourceRequest("verb1").Group("group2").Resource("resource2").New(), true},
resourceRequest("verb2").Group("group1").Resource("resource1").New(): true, {resourceRequest("verb2").Group("group1").Resource("resource1").New(), true},
resourceRequest("verb2").Group("group2").Resource("resource1").New(): true, {resourceRequest("verb2").Group("group2").Resource("resource1").New(), true},
resourceRequest("verb2").Group("group1").Resource("resource2").New(): true, {resourceRequest("verb2").Group("group1").Resource("resource2").New(), true},
resourceRequest("verb2").Group("group2").Resource("resource2").New(): true, {resourceRequest("verb2").Group("group2").Resource("resource2").New(), true},
}, },
}, },
{ {
name: "subresource expansion", name: "subresource expansion",
rule: rbacv1helpers.NewRule("*").Groups("*").Resources("resource1/subresource1").RuleOrDie(), rule: rbacv1helpers.NewRule("*").Groups("*").Resources("resource1/subresource1").RuleOrDie(),
requestsToExpected: map[authorizer.AttributesRecord]bool{ requestsToExpected: []*requestToTest{
resourceRequest("verb1").Group("group1").Resource("resource1").Subresource("subresource1").New(): true, {resourceRequest("verb1").Group("group1").Resource("resource1").Subresource("subresource1").New(), true},
resourceRequest("verb1").Group("group2").Resource("resource1").Subresource("subresource2").New(): false, {resourceRequest("verb1").Group("group2").Resource("resource1").Subresource("subresource2").New(), false},
resourceRequest("verb1").Group("group1").Resource("resource2").Subresource("subresource1").New(): false, {resourceRequest("verb1").Group("group1").Resource("resource2").Subresource("subresource1").New(), false},
resourceRequest("verb1").Group("group2").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("group1").Resource("resource1").Subresource("subresource1").New(), true},
resourceRequest("verb2").Group("group2").Resource("resource1").Subresource("subresource2").New(): false, {resourceRequest("verb2").Group("group2").Resource("resource1").Subresource("subresource2").New(), false},
resourceRequest("verb2").Group("group1").Resource("resource2").Subresource("subresource1").New(): false, {resourceRequest("verb2").Group("group1").Resource("resource2").Subresource("subresource1").New(), false},
resourceRequest("verb2").Group("group2").Resource("resource2").Subresource("subresource1").New(): false, {resourceRequest("verb2").Group("group2").Resource("resource2").Subresource("subresource1").New(), false},
}, },
}, },
{ {
name: "star nonresource, exact match other", name: "star nonresource, exact match other",
rule: rbacv1helpers.NewRule("verb1").URLs("*").RuleOrDie(), rule: rbacv1helpers.NewRule("verb1").URLs("*").RuleOrDie(),
requestsToExpected: map[authorizer.AttributesRecord]bool{ requestsToExpected: []*requestToTest{
nonresourceRequest("verb1").URL("/foo").New(): true, {nonresourceRequest("verb1").URL("/foo").New(), true},
nonresourceRequest("verb1").URL("/foo/bar").New(): true, {nonresourceRequest("verb1").URL("/foo/bar").New(), true},
nonresourceRequest("verb1").URL("/foo/baz").New(): true, {nonresourceRequest("verb1").URL("/foo/baz").New(), true},
nonresourceRequest("verb1").URL("/foo/bar/one").New(): true, {nonresourceRequest("verb1").URL("/foo/bar/one").New(), true},
nonresourceRequest("verb1").URL("/foo/baz/one").New(): true, {nonresourceRequest("verb1").URL("/foo/baz/one").New(), true},
nonresourceRequest("verb2").URL("/foo").New(): false, {nonresourceRequest("verb2").URL("/foo").New(), false},
nonresourceRequest("verb2").URL("/foo/bar").New(): false, {nonresourceRequest("verb2").URL("/foo/bar").New(), false},
nonresourceRequest("verb2").URL("/foo/baz").New(): false, {nonresourceRequest("verb2").URL("/foo/baz").New(), false},
nonresourceRequest("verb2").URL("/foo/bar/one").New(): false, {nonresourceRequest("verb2").URL("/foo/bar/one").New(), false},
nonresourceRequest("verb2").URL("/foo/baz/one").New(): false, {nonresourceRequest("verb2").URL("/foo/baz/one").New(), false},
}, },
}, },
{ {
name: "star nonresource subpath", name: "star nonresource subpath",
rule: rbacv1helpers.NewRule("verb1").URLs("/foo/*").RuleOrDie(), rule: rbacv1helpers.NewRule("verb1").URLs("/foo/*").RuleOrDie(),
requestsToExpected: map[authorizer.AttributesRecord]bool{ requestsToExpected: []*requestToTest{
nonresourceRequest("verb1").URL("/foo").New(): false, {nonresourceRequest("verb1").URL("/foo").New(), false},
nonresourceRequest("verb1").URL("/foo/bar").New(): true, {nonresourceRequest("verb1").URL("/foo/bar").New(), true},
nonresourceRequest("verb1").URL("/foo/baz").New(): true, {nonresourceRequest("verb1").URL("/foo/baz").New(), true},
nonresourceRequest("verb1").URL("/foo/bar/one").New(): true, {nonresourceRequest("verb1").URL("/foo/bar/one").New(), true},
nonresourceRequest("verb1").URL("/foo/baz/one").New(): true, {nonresourceRequest("verb1").URL("/foo/baz/one").New(), true},
nonresourceRequest("verb1").URL("/notfoo").New(): false, {nonresourceRequest("verb1").URL("/notfoo").New(), false},
nonresourceRequest("verb1").URL("/notfoo/bar").New(): false, {nonresourceRequest("verb1").URL("/notfoo/bar").New(), false},
nonresourceRequest("verb1").URL("/notfoo/baz").New(): false, {nonresourceRequest("verb1").URL("/notfoo/baz").New(), false},
nonresourceRequest("verb1").URL("/notfoo/bar/one").New(): false, {nonresourceRequest("verb1").URL("/notfoo/bar/one").New(), false},
nonresourceRequest("verb1").URL("/notfoo/baz/one").New(): false, {nonresourceRequest("verb1").URL("/notfoo/baz/one").New(), false},
}, },
}, },
{ {
name: "star verb, exact nonresource", name: "star verb, exact nonresource",
rule: rbacv1helpers.NewRule("*").URLs("/foo", "/foo/bar/one").RuleOrDie(), rule: rbacv1helpers.NewRule("*").URLs("/foo", "/foo/bar/one").RuleOrDie(),
requestsToExpected: map[authorizer.AttributesRecord]bool{ requestsToExpected: []*requestToTest{
nonresourceRequest("verb1").URL("/foo").New(): true, {nonresourceRequest("verb1").URL("/foo").New(), true},
nonresourceRequest("verb1").URL("/foo/bar").New(): false, {nonresourceRequest("verb1").URL("/foo/bar").New(), false},
nonresourceRequest("verb1").URL("/foo/baz").New(): false, {nonresourceRequest("verb1").URL("/foo/baz").New(), false},
nonresourceRequest("verb1").URL("/foo/bar/one").New(): true, {nonresourceRequest("verb1").URL("/foo/bar/one").New(), true},
nonresourceRequest("verb1").URL("/foo/baz/one").New(): false, {nonresourceRequest("verb1").URL("/foo/baz/one").New(), false},
nonresourceRequest("verb2").URL("/foo").New(): true, {nonresourceRequest("verb2").URL("/foo").New(), true},
nonresourceRequest("verb2").URL("/foo/bar").New(): false, {nonresourceRequest("verb2").URL("/foo/bar").New(), false},
nonresourceRequest("verb2").URL("/foo/baz").New(): false, {nonresourceRequest("verb2").URL("/foo/baz").New(), false},
nonresourceRequest("verb2").URL("/foo/bar/one").New(): true, {nonresourceRequest("verb2").URL("/foo/bar/one").New(), true},
nonresourceRequest("verb2").URL("/foo/baz/one").New(): false, {nonresourceRequest("verb2").URL("/foo/baz/one").New(), false},
}, },
}, },
} }
for _, tc := range tests { for _, tc := range tests {
for request, expected := range tc.requestsToExpected { for _, requestToTest := range tc.requestsToExpected {
if e, a := expected, RuleAllows(request, &tc.rule); e != a { 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, request) t.Errorf("%q: expected %v, got %v for %v", tc.name, e, a, requestToTest.request)
} }
} }
} }

View File

@ -32,6 +32,10 @@ import (
type LabelSelectorValidationOptions struct { type LabelSelectorValidationOptions struct {
// Allow invalid label value in selector // Allow invalid label value in selector
AllowInvalidLabelValueInSelector bool 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. // 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'")) allErrs = append(allErrs, field.Forbidden(fldPath.Child("values"), "may not be specified when `operator` is 'Exists' or 'DoesNotExist'"))
} }
default: default:
if !opts.AllowUnknownOperatorInRequirement {
allErrs = append(allErrs, field.Invalid(fldPath.Child("operator"), sr.Operator, "not a valid selector operator")) allErrs = append(allErrs, field.Invalid(fldPath.Child("operator"), sr.Operator, "not a valid selector operator"))
} }
}
allErrs = append(allErrs, ValidateLabelName(sr.Key, fldPath.Child("key"))...) allErrs = append(allErrs, ValidateLabelName(sr.Key, fldPath.Child("key"))...)
if !opts.AllowInvalidLabelValueInSelector { if !opts.AllowInvalidLabelValueInSelector {
for valueIndex, value := range sr.Values { for valueIndex, value := range sr.Values {
@ -113,6 +119,39 @@ func ValidateLabels(labels map[string]string, fldPath *field.Path) field.ErrorLi
return allErrs 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 { func ValidateDeleteOptions(options *metav1.DeleteOptions) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
//lint:file-ignore SA1019 Keep validation for deprecated OrphanDependents option until it's being removed //lint:file-ignore SA1019 Keep validation for deprecated OrphanDependents option until it's being removed

View File

@ -470,7 +470,7 @@ func TestLabelSelectorMatchExpression(t *testing.T) {
}} }}
for index, testCase := range testCases { for index, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) { 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 { if len(allErrs) != testCase.wantErrorNumber {
t.Errorf("case[%d]: expected failure", index) t.Errorf("case[%d]: expected failure", index)
} }

View File

@ -45,6 +45,19 @@ var (
// Requirements is AND of all requirements. // Requirements is AND of all requirements.
type Requirements []Requirement 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. // Selector represents a label selector.
type Selector interface { type Selector interface {
// Matches returns true if this selector matches the given set of labels. // Matches returns true if this selector matches the given set of labels.
@ -285,6 +298,13 @@ func (r *Requirement) Values() sets.String {
return ret 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. // Equal checks the equality of requirement.
func (r Requirement) Equal(x Requirement) bool { func (r Requirement) Equal(x Requirement) bool {
if r.key != x.key { if r.key != x.key {

View File

@ -22,6 +22,8 @@ import (
"sort" "sort"
"strings" "strings"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
) )
@ -58,6 +60,8 @@ var _ authorizer.Attributes = (interface {
GetAPIVersion() string GetAPIVersion() string
IsResourceRequest() bool IsResourceRequest() bool
GetPath() string GetPath() string
GetFieldSelector() (fields.Requirements, error)
GetLabelSelector() (labels.Requirements, error)
})(nil) })(nil)
// The user info accessors known to cache key construction. If this fails to compile, the cache // The user info accessors known to cache key construction. If this fails to compile, the cache
@ -72,7 +76,13 @@ var _ user.Info = (interface {
// Authorize returns an authorization decision by delegating to another Authorizer. If an equivalent // Authorize returns an authorization decision by delegating to another Authorizer. If an equivalent
// check has already been performed, a cached result is returned. Not safe for concurrent use. // check has already been performed, a cached result is returned. Not safe for concurrent use.
func (ca *cachingAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { func (ca *cachingAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
serializableAttributes := authorizer.AttributesRecord{ type SerializableAttributes struct {
authorizer.AttributesRecord
LabelSelector string
}
serializableAttributes := SerializableAttributes{
AttributesRecord: authorizer.AttributesRecord{
Verb: a.GetVerb(), Verb: a.GetVerb(),
Namespace: a.GetNamespace(), Namespace: a.GetNamespace(),
APIGroup: a.GetAPIGroup(), APIGroup: a.GetAPIGroup(),
@ -82,6 +92,15 @@ func (ca *cachingAuthorizer) Authorize(ctx context.Context, a authorizer.Attribu
Name: a.GetName(), Name: a.GetName(),
ResourceRequest: a.IsResourceRequest(), ResourceRequest: a.IsResourceRequest(),
Path: a.GetPath(), Path: a.GetPath(),
},
}
// in the error case, we won't honor this field selector, so the cache doesn't need it.
if fieldSelector, err := a.GetFieldSelector(); len(fieldSelector) > 0 {
serializableAttributes.FieldSelectorRequirements, serializableAttributes.FieldSelectorParsingErr = fieldSelector, err
}
if labelSelector, _ := a.GetLabelSelector(); len(labelSelector) > 0 {
// the labels requirements have private elements so those don't help us serialize to a unique key
serializableAttributes.LabelSelector = labelSelector.String()
} }
if u := a.GetUser(); u != nil { if u := a.GetUser(); u != nil {

View File

@ -18,14 +18,31 @@ package validating
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"testing" "testing"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
) )
func mustParseLabelSelector(str string) labels.Requirements {
ret, err := labels.Parse(str)
if err != nil {
panic(err)
}
retRequirements, _ /*selectable*/ := ret.Requirements()
return retRequirements
}
func TestCachingAuthorizer(t *testing.T) { func TestCachingAuthorizer(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
type result struct { type result struct {
decision authorizer.Decision decision authorizer.Decision
reason string reason string
@ -216,6 +233,261 @@ func TestCachingAuthorizer(t *testing.T) {
}, },
}, },
}, },
{
name: "honor good field selector",
calls: []invocation{
{
attributes: authorizer.AttributesRecord{
Name: "test name",
FieldSelectorRequirements: fields.ParseSelectorOrDie("foo=bar").Requirements(),
},
expected: result{
decision: authorizer.DecisionAllow,
reason: "test reason",
error: fmt.Errorf("test error"),
},
},
{
attributes: authorizer.AttributesRecord{
Name: "test name",
},
expected: result{
decision: authorizer.DecisionAllow,
reason: "test reason 2",
error: fmt.Errorf("test error 2"),
},
},
{
// now this should be cached
attributes: authorizer.AttributesRecord{
Name: "test name",
FieldSelectorRequirements: fields.ParseSelectorOrDie("foo=bar").Requirements(),
},
expected: result{
decision: authorizer.DecisionAllow,
reason: "test reason",
error: fmt.Errorf("test error"),
},
},
},
backend: []result{
{
decision: authorizer.DecisionAllow,
reason: "test reason",
error: fmt.Errorf("test error"),
},
{
decision: authorizer.DecisionAllow,
reason: "test reason 2",
error: fmt.Errorf("test error 2"),
},
},
},
{
name: "ignore malformed field selector first",
calls: []invocation{
{
attributes: authorizer.AttributesRecord{
Name: "test name",
FieldSelectorParsingErr: errors.New("malformed"),
},
expected: result{
decision: authorizer.DecisionAllow,
reason: "test reason",
error: fmt.Errorf("test error"),
},
},
{
// notice that this does not have the malformed field selector.
// it should use the cached result
attributes: authorizer.AttributesRecord{
Name: "test name",
},
expected: result{
decision: authorizer.DecisionAllow,
reason: "test reason",
error: fmt.Errorf("test error"),
},
},
},
backend: []result{
{
decision: authorizer.DecisionAllow,
reason: "test reason",
error: fmt.Errorf("test error"),
},
},
},
{
name: "ignore malformed field selector second",
calls: []invocation{
{
attributes: authorizer.AttributesRecord{
Name: "test name",
},
expected: result{
decision: authorizer.DecisionAllow,
reason: "test reason",
error: fmt.Errorf("test error"),
},
},
{
// this should use the broader cached value because the selector will be ignored
attributes: authorizer.AttributesRecord{
Name: "test name",
FieldSelectorParsingErr: errors.New("malformed"),
},
expected: result{
decision: authorizer.DecisionAllow,
reason: "test reason",
error: fmt.Errorf("test error"),
},
},
},
backend: []result{
{
decision: authorizer.DecisionAllow,
reason: "test reason",
error: fmt.Errorf("test error"),
},
},
},
{
name: "honor good label selector",
calls: []invocation{
{
attributes: authorizer.AttributesRecord{
Name: "test name",
LabelSelectorRequirements: mustParseLabelSelector("foo=bar"),
},
expected: result{
decision: authorizer.DecisionAllow,
reason: "test reason",
error: fmt.Errorf("test error"),
},
},
{
attributes: authorizer.AttributesRecord{
Name: "test name",
},
expected: result{
decision: authorizer.DecisionAllow,
reason: "test reason 2",
error: fmt.Errorf("test error 2"),
},
},
{
// now this should be cached
attributes: authorizer.AttributesRecord{
Name: "test name",
LabelSelectorRequirements: mustParseLabelSelector("foo=bar"),
},
expected: result{
decision: authorizer.DecisionAllow,
reason: "test reason",
error: fmt.Errorf("test error"),
},
},
{
attributes: authorizer.AttributesRecord{
Name: "test name",
LabelSelectorRequirements: mustParseLabelSelector("diff=zero"),
},
expected: result{
decision: authorizer.DecisionAllow,
reason: "test reason 3",
error: fmt.Errorf("test error 3"),
},
},
},
backend: []result{
{
decision: authorizer.DecisionAllow,
reason: "test reason",
error: fmt.Errorf("test error"),
},
{
decision: authorizer.DecisionAllow,
reason: "test reason 2",
error: fmt.Errorf("test error 2"),
},
{
decision: authorizer.DecisionAllow,
reason: "test reason 3",
error: fmt.Errorf("test error 3"),
},
},
},
{
name: "ignore malformed label selector first",
calls: []invocation{
{
attributes: authorizer.AttributesRecord{
Name: "test name",
LabelSelectorParsingErr: errors.New("malformed mess"),
},
expected: result{
decision: authorizer.DecisionAllow,
reason: "test reason",
error: fmt.Errorf("test error"),
},
},
{
// notice that this does not have the malformed field selector.
// it should use the cached result
attributes: authorizer.AttributesRecord{
Name: "test name",
},
expected: result{
decision: authorizer.DecisionAllow,
reason: "test reason",
error: fmt.Errorf("test error"),
},
},
},
backend: []result{
{
decision: authorizer.DecisionAllow,
reason: "test reason",
error: fmt.Errorf("test error"),
},
},
},
{
name: "ignore malformed label selector second",
calls: []invocation{
{
attributes: authorizer.AttributesRecord{
Name: "test name",
},
expected: result{
decision: authorizer.DecisionAllow,
reason: "test reason",
error: fmt.Errorf("test error"),
},
},
{
// this should use the broader cached value because the selector will be ignored
attributes: authorizer.AttributesRecord{
Name: "test name",
LabelSelectorParsingErr: errors.New("malformed mess"),
},
expected: result{
decision: authorizer.DecisionAllow,
reason: "test reason",
error: fmt.Errorf("test error"),
},
},
},
backend: []result{
{
decision: authorizer.DecisionAllow,
reason: "test reason",
error: fmt.Errorf("test error"),
},
},
},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
var misses int var misses int

View File

@ -20,6 +20,8 @@ import (
"context" "context"
"net/http" "net/http"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
) )
@ -62,6 +64,16 @@ type Attributes interface {
// GetPath returns the path of the request // GetPath returns the path of the request
GetPath() string GetPath() string
// ParseFieldSelector is lazy, thread-safe, and stores the parsed result and error.
// It returns an error if the field selector cannot be parsed.
// The returned requirements must be treated as readonly and not modified.
GetFieldSelector() (fields.Requirements, error)
// ParseLabelSelector is lazy, thread-safe, and stores the parsed result and error.
// It returns an error if the label selector cannot be parsed.
// The returned requirements must be treated as readonly and not modified.
GetLabelSelector() (labels.Requirements, error)
} }
// Authorizer makes an authorization decision based on information gained by making // Authorizer makes an authorization decision based on information gained by making
@ -100,6 +112,11 @@ type AttributesRecord struct {
Name string Name string
ResourceRequest bool ResourceRequest bool
Path string Path string
FieldSelectorRequirements fields.Requirements
FieldSelectorParsingErr error
LabelSelectorRequirements labels.Requirements
LabelSelectorParsingErr error
} }
func (a AttributesRecord) GetUser() user.Info { func (a AttributesRecord) GetUser() user.Info {
@ -146,6 +163,14 @@ func (a AttributesRecord) GetPath() string {
return a.Path return a.Path
} }
func (a AttributesRecord) GetFieldSelector() (fields.Requirements, error) {
return a.FieldSelectorRequirements, a.FieldSelectorParsingErr
}
func (a AttributesRecord) GetLabelSelector() (labels.Requirements, error) {
return a.LabelSelectorRequirements, a.LabelSelectorParsingErr
}
type Decision int type Decision int
const ( const (

View File

@ -22,6 +22,11 @@ import (
"net/http" "net/http"
"time" "time"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/klog/v2" "k8s.io/klog/v2"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -117,5 +122,31 @@ func GetAuthorizerAttributes(ctx context.Context) (authorizer.Attributes, error)
attribs.Namespace = requestInfo.Namespace attribs.Namespace = requestInfo.Namespace
attribs.Name = requestInfo.Name attribs.Name = requestInfo.Name
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) {
// parsing here makes it easy to keep the AttributesRecord type value-only and avoids any mutex copies when
// doing shallow copies in other steps.
if len(requestInfo.FieldSelector) > 0 {
fieldSelector, err := fields.ParseSelector(requestInfo.FieldSelector)
if err != nil {
attribs.FieldSelectorRequirements, attribs.FieldSelectorParsingErr = nil, err
} else {
if requirements := fieldSelector.Requirements(); len(requirements) > 0 {
attribs.FieldSelectorRequirements, attribs.FieldSelectorParsingErr = fieldSelector.Requirements(), nil
}
}
}
if len(requestInfo.LabelSelector) > 0 {
labelSelector, err := labels.Parse(requestInfo.LabelSelector)
if err != nil {
attribs.LabelSelectorRequirements, attribs.LabelSelectorParsingErr = nil, err
} else {
if requirements, _ /*selectable*/ := labelSelector.Requirements(); len(requirements) > 0 {
attribs.LabelSelectorRequirements, attribs.LabelSelectorParsingErr = requirements, nil
}
}
}
}
return &attribs, nil return &attribs, nil
} }

View File

@ -19,6 +19,12 @@ package filters
import ( import (
"context" "context"
"errors" "errors"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect" "reflect"
@ -34,10 +40,16 @@ import (
) )
func TestGetAuthorizerAttributes(t *testing.T) { func TestGetAuthorizerAttributes(t *testing.T) {
basicLabelRequirement, err := labels.NewRequirement("foo", selection.DoubleEquals, []string{"bar"})
if err != nil {
t.Fatal(err)
}
testcases := map[string]struct { testcases := map[string]struct {
Verb string Verb string
Path string Path string
ExpectedAttributes *authorizer.AttributesRecord ExpectedAttributes *authorizer.AttributesRecord
EnableAuthorizationSelector bool
}{ }{
"non-resource root": { "non-resource root": {
Verb: "POST", Verb: "POST",
@ -102,9 +114,104 @@ func TestGetAuthorizerAttributes(t *testing.T) {
Resource: "jobs", Resource: "jobs",
}, },
}, },
"disabled, ignore good field selector": {
Verb: "GET",
Path: "/apis/batch/v1/namespaces/myns/jobs?fieldSelector%=foo%3Dbar",
ExpectedAttributes: &authorizer.AttributesRecord{
Verb: "list",
Path: "/apis/batch/v1/namespaces/myns/jobs",
ResourceRequest: true,
APIGroup: batch.GroupName,
APIVersion: "v1",
Namespace: "myns",
Resource: "jobs",
},
},
"enabled, good field selector": {
Verb: "GET",
Path: "/apis/batch/v1/namespaces/myns/jobs?fieldSelector=foo%3D%3Dbar",
ExpectedAttributes: &authorizer.AttributesRecord{
Verb: "list",
Path: "/apis/batch/v1/namespaces/myns/jobs",
ResourceRequest: true,
APIGroup: batch.GroupName,
APIVersion: "v1",
Namespace: "myns",
Resource: "jobs",
FieldSelectorRequirements: fields.Requirements{
fields.OneTermEqualSelector("foo", "bar").Requirements()[0],
},
},
EnableAuthorizationSelector: true,
},
"enabled, bad field selector": {
Verb: "GET",
Path: "/apis/batch/v1/namespaces/myns/jobs?fieldSelector=%2Abar",
ExpectedAttributes: &authorizer.AttributesRecord{
Verb: "list",
Path: "/apis/batch/v1/namespaces/myns/jobs",
ResourceRequest: true,
APIGroup: batch.GroupName,
APIVersion: "v1",
Namespace: "myns",
Resource: "jobs",
FieldSelectorParsingErr: errors.New("invalid selector: '*bar'; can't understand '*bar'"),
},
EnableAuthorizationSelector: true,
},
"disabled, ignore good label selector": {
Verb: "GET",
Path: "/apis/batch/v1/namespaces/myns/jobs?labelSelector%=foo%3Dbar",
ExpectedAttributes: &authorizer.AttributesRecord{
Verb: "list",
Path: "/apis/batch/v1/namespaces/myns/jobs",
ResourceRequest: true,
APIGroup: batch.GroupName,
APIVersion: "v1",
Namespace: "myns",
Resource: "jobs",
},
},
"enabled, good label selector": {
Verb: "GET",
Path: "/apis/batch/v1/namespaces/myns/jobs?labelSelector=foo%3D%3Dbar",
ExpectedAttributes: &authorizer.AttributesRecord{
Verb: "list",
Path: "/apis/batch/v1/namespaces/myns/jobs",
ResourceRequest: true,
APIGroup: batch.GroupName,
APIVersion: "v1",
Namespace: "myns",
Resource: "jobs",
LabelSelectorRequirements: labels.Requirements{
*basicLabelRequirement,
},
},
EnableAuthorizationSelector: true,
},
"enabled, bad label selector": {
Verb: "GET",
Path: "/apis/batch/v1/namespaces/myns/jobs?labelSelector=%2Abar",
ExpectedAttributes: &authorizer.AttributesRecord{
Verb: "list",
Path: "/apis/batch/v1/namespaces/myns/jobs",
ResourceRequest: true,
APIGroup: batch.GroupName,
APIVersion: "v1",
Namespace: "myns",
Resource: "jobs",
LabelSelectorParsingErr: errors.New("unable to parse requirement: <nil>: Invalid value: \"*bar\": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')"),
},
EnableAuthorizationSelector: true,
},
} }
for k, tc := range testcases { for k, tc := range testcases {
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, _ := http.NewRequest(tc.Verb, tc.Path, nil)
req.RemoteAddr = "127.0.0.1" req.RemoteAddr = "127.0.0.1"
@ -122,6 +229,7 @@ func TestGetAuthorizerAttributes(t *testing.T) {
} else if !reflect.DeepEqual(attribs, tc.ExpectedAttributes) { } else if !reflect.DeepEqual(attribs, tc.ExpectedAttributes) {
t.Errorf("%s: expected\n\t%#v\ngot\n\t%#v", k, tc.ExpectedAttributes, attribs) t.Errorf("%s: expected\n\t%#v\ngot\n\t%#v", k, tc.ExpectedAttributes, attribs)
} }
})
} }
} }

View File

@ -27,6 +27,8 @@ import (
metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme" metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/klog/v2" "k8s.io/klog/v2"
) )
@ -62,6 +64,13 @@ type RequestInfo struct {
Name string Name string
// Parts are the path parts for the request, always starting with /{resource}/{name} // Parts are the path parts for the request, always starting with /{resource}/{name}
Parts []string Parts []string
// FieldSelector contains the unparsed field selector from a request. It is only present if the apiserver
// honors field selectors for the verb this request is associated with.
FieldSelector string
// LabelSelector contains the unparsed field selector from a request. It is only present if the apiserver
// honors field selectors for the verb this request is associated with.
LabelSelector string
} }
// specialVerbs contains just strings which are used in REST paths for special actions that don't fall under the normal // specialVerbs contains just strings which are used in REST paths for special actions that don't fall under the normal
@ -77,6 +86,9 @@ var specialVerbsNoSubresources = sets.NewString("proxy")
// this list allows the parser to distinguish between a namespace subresource, and a namespaced resource // this list allows the parser to distinguish between a namespace subresource, and a namespaced resource
var namespaceSubresources = sets.NewString("status", "finalize") var namespaceSubresources = sets.NewString("status", "finalize")
// verbsWithSelectors is the list of verbs which support fieldSelector and labelSelector parameters
var verbsWithSelectors = sets.NewString("list", "watch", "deletecollection")
// NamespaceSubResourcesForTest exports namespaceSubresources for testing in pkg/controlplane/master_test.go, so we never drift // NamespaceSubResourcesForTest exports namespaceSubresources for testing in pkg/controlplane/master_test.go, so we never drift
var NamespaceSubResourcesForTest = sets.NewString(namespaceSubresources.List()...) var NamespaceSubResourcesForTest = sets.NewString(namespaceSubresources.List()...)
@ -151,6 +163,7 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er
currentParts = currentParts[1:] currentParts = currentParts[1:]
// handle input of form /{specialVerb}/* // handle input of form /{specialVerb}/*
verbViaPathPrefix := false
if specialVerbs.Has(currentParts[0]) { if specialVerbs.Has(currentParts[0]) {
if len(currentParts) < 2 { if len(currentParts) < 2 {
return &requestInfo, fmt.Errorf("unable to determine kind and namespace from url, %v", req.URL) return &requestInfo, fmt.Errorf("unable to determine kind and namespace from url, %v", req.URL)
@ -158,6 +171,7 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er
requestInfo.Verb = currentParts[0] requestInfo.Verb = currentParts[0]
currentParts = currentParts[1:] currentParts = currentParts[1:]
verbViaPathPrefix = true
} else { } else {
switch req.Method { switch req.Method {
@ -238,11 +252,28 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er
} }
} }
} }
// if there's no name on the request and we thought it was a delete before, then the actual verb is deletecollection // if there's no name on the request and we thought it was a delete before, then the actual verb is deletecollection
if len(requestInfo.Name) == 0 && requestInfo.Verb == "delete" { if len(requestInfo.Name) == 0 && requestInfo.Verb == "delete" {
requestInfo.Verb = "deletecollection" requestInfo.Verb = "deletecollection"
} }
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) {
// Don't support selector authorization on requests that used the deprecated verb-via-path mechanism, since they don't support selectors consistently.
// There are multi-object and single-object watch endpoints, and only the multi-object one supports selectors.
if !verbViaPathPrefix && verbsWithSelectors.Has(requestInfo.Verb) {
// interestingly these are parsed above, but the current structure there means that if one (or anything) in the
// listOptions fails to decode, the field and label selectors are lost.
// therefore, do the straight query param read here.
if vals := req.URL.Query()["fieldSelector"]; len(vals) > 0 {
requestInfo.FieldSelector = vals[0]
}
if vals := req.URL.Query()["labelSelector"]; len(vals) > 0 {
requestInfo.LabelSelector = vals[0]
}
}
}
return &requestInfo, nil return &requestInfo, nil
} }

View File

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

View File

@ -95,6 +95,13 @@ const (
// Enables serving watch requests in separate goroutines. // Enables serving watch requests in separate goroutines.
APIServingWithRoutine featuregate.Feature = "APIServingWithRoutine" APIServingWithRoutine featuregate.Feature = "APIServingWithRoutine"
// owner: @deads2k
// kep: https://kep.k8s.io/4601
// alpha: v1.31
//
// Allows authorization to use field and label selectors.
AuthorizeWithSelectors featuregate.Feature = "AuthorizeWithSelectors"
// owner: @cici37 @jpbetz // owner: @cici37 @jpbetz
// kep: http://kep.k8s.io/3488 // kep: http://kep.k8s.io/3488
// alpha: v1.26 // alpha: v1.26
@ -358,6 +365,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
APIServingWithRoutine: {Default: true, PreRelease: featuregate.Beta}, APIServingWithRoutine: {Default: true, PreRelease: featuregate.Beta},
AuthorizeWithSelectors: {Default: false, PreRelease: featuregate.Alpha},
ValidatingAdmissionPolicy: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.32 ValidatingAdmissionPolicy: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.32
CustomResourceValidationExpressions: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.31 CustomResourceValidationExpressions: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.31

View File

@ -88,6 +88,8 @@ func v1beta1ResourceAttributesToV1ResourceAttributes(in *authorizationv1beta1.Re
Resource: in.Resource, Resource: in.Resource,
Subresource: in.Subresource, Subresource: in.Subresource,
Name: in.Name, Name: in.Name,
FieldSelector: in.FieldSelector,
LabelSelector: in.LabelSelector,
} }
} }

View File

@ -32,6 +32,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/util/cache" "k8s.io/apimachinery/pkg/util/cache"
utilnet "k8s.io/apimachinery/pkg/util/net" utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
@ -40,7 +41,7 @@ import (
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
authorizationcel "k8s.io/apiserver/pkg/authorization/cel" authorizationcel "k8s.io/apiserver/pkg/authorization/cel"
"k8s.io/apiserver/pkg/features" genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/apiserver/pkg/util/webhook" "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/apiserver/plugin/pkg/authorizer/webhook/metrics" "k8s.io/apiserver/plugin/pkg/authorizer/webhook/metrics"
@ -196,15 +197,7 @@ func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attri
} }
if attr.IsResourceRequest() { if attr.IsResourceRequest() {
r.Spec.ResourceAttributes = &authorizationv1.ResourceAttributes{ r.Spec.ResourceAttributes = resourceAttributesFrom(attr)
Namespace: attr.GetNamespace(),
Verb: attr.GetVerb(),
Group: attr.GetAPIGroup(),
Version: attr.GetAPIVersion(),
Resource: attr.GetResource(),
Subresource: attr.GetSubresource(),
Name: attr.GetName(),
}
} else { } else {
r.Spec.NonResourceAttributes = &authorizationv1.NonResourceAttributes{ r.Spec.NonResourceAttributes = &authorizationv1.NonResourceAttributes{
Path: attr.GetPath(), Path: attr.GetPath(),
@ -212,7 +205,7 @@ func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attri
} }
} }
// skipping match when feature is not enabled // skipping match when feature is not enabled
if utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthorizationConfiguration) { if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StructuredAuthorizationConfiguration) {
// Process Match Conditions before calling the webhook // Process Match Conditions before calling the webhook
matches, err := w.match(ctx, r) matches, err := w.match(ctx, r)
// If at least one matchCondition evaluates to an error (but none are FALSE): // If at least one matchCondition evaluates to an error (but none are FALSE):
@ -305,6 +298,109 @@ func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attri
} }
func resourceAttributesFrom(attr authorizer.Attributes) *authorizationv1.ResourceAttributes {
ret := &authorizationv1.ResourceAttributes{
Namespace: attr.GetNamespace(),
Verb: attr.GetVerb(),
Group: attr.GetAPIGroup(),
Version: attr.GetAPIVersion(),
Resource: attr.GetResource(),
Subresource: attr.GetSubresource(),
Name: attr.GetName(),
}
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) {
// If we are able to get any requirements while parsing selectors, use them, even if there's an error.
// This is because selectors only narrow, so if a subset of selector requirements are available, the request can be allowed.
if selectorRequirements, _ := fieldSelectorToAuthorizationAPI(attr); len(selectorRequirements) > 0 {
ret.FieldSelector = &authorizationv1.FieldSelectorAttributes{
Requirements: selectorRequirements,
}
}
if selectorRequirements, _ := labelSelectorToAuthorizationAPI(attr); len(selectorRequirements) > 0 {
ret.LabelSelector = &authorizationv1.LabelSelectorAttributes{
Requirements: selectorRequirements,
}
}
}
return ret
}
func fieldSelectorToAuthorizationAPI(attr authorizer.Attributes) ([]metav1.FieldSelectorRequirement, error) {
requirements, getFieldSelectorErr := attr.GetFieldSelector()
if len(requirements) == 0 {
return nil, getFieldSelectorErr
}
retRequirements := []metav1.FieldSelectorRequirement{}
for _, requirement := range requirements {
retRequirement := metav1.FieldSelectorRequirement{}
switch {
case requirement.Operator == selection.Equals || requirement.Operator == selection.DoubleEquals || requirement.Operator == selection.In:
retRequirement.Operator = metav1.FieldSelectorOpIn
retRequirement.Key = requirement.Field
retRequirement.Values = []string{requirement.Value}
case requirement.Operator == selection.NotEquals || requirement.Operator == selection.NotIn:
retRequirement.Operator = metav1.FieldSelectorOpNotIn
retRequirement.Key = requirement.Field
retRequirement.Values = []string{requirement.Value}
default:
// ignore this particular requirement. since requirements are AND'd, it is safe to ignore unknown requirements
// for authorization since the resulting check will only be as broad or broader than the intended.
continue
}
retRequirements = append(retRequirements, retRequirement)
}
if len(retRequirements) == 0 {
// this means that all requirements were dropped (likely due to unknown operators), so we are checking the broader
// unrestricted action.
return nil, getFieldSelectorErr
}
return retRequirements, getFieldSelectorErr
}
func labelSelectorToAuthorizationAPI(attr authorizer.Attributes) ([]metav1.LabelSelectorRequirement, error) {
requirements, getLabelSelectorErr := attr.GetLabelSelector()
if len(requirements) == 0 {
return nil, getLabelSelectorErr
}
retRequirements := []metav1.LabelSelectorRequirement{}
for _, requirement := range requirements {
retRequirement := metav1.LabelSelectorRequirement{
Key: requirement.Key(),
}
if values := requirement.ValuesUnsorted(); len(values) > 0 {
retRequirement.Values = values
}
switch requirement.Operator() {
case selection.Equals, selection.DoubleEquals, selection.In:
retRequirement.Operator = metav1.LabelSelectorOpIn
case selection.NotEquals, selection.NotIn:
retRequirement.Operator = metav1.LabelSelectorOpNotIn
case selection.Exists:
retRequirement.Operator = metav1.LabelSelectorOpExists
case selection.DoesNotExist:
retRequirement.Operator = metav1.LabelSelectorOpDoesNotExist
default:
// ignore this particular requirement. since requirements are AND'd, it is safe to ignore unknown requirements
// for authorization since the resulting check will only be as broad or broader than the intended.
continue
}
retRequirements = append(retRequirements, retRequirement)
}
if len(retRequirements) == 0 {
// this means that all requirements were dropped (likely due to unknown operators), so we are checking the broader
// unrestricted action.
return nil, getLabelSelectorErr
}
return retRequirements, getLabelSelectorErr
}
// TODO: need to finish the method to get the rules when using webhook mode // TODO: need to finish the method to get the rules when using webhook mode
func (w *WebhookAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) { func (w *WebhookAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
var ( var (
@ -482,6 +578,8 @@ func v1ResourceAttributesToV1beta1ResourceAttributes(in *authorizationv1.Resourc
Resource: in.Resource, Resource: in.Resource,
Subresource: in.Subresource, Subresource: in.Subresource,
Name: in.Name, Name: in.Name,
FieldSelector: in.FieldSelector,
LabelSelector: in.LabelSelector,
} }
} }

View File

@ -0,0 +1,334 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webhook
import (
"errors"
"reflect"
"testing"
authorizationv1 "k8s.io/api/authorization/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
)
func mustLabelRequirement(selector string) labels.Requirements {
ret, err := labels.Parse(selector)
if err != nil {
panic(err)
}
requirements, _ := ret.Requirements()
return requirements
}
func Test_resourceAttributesFrom(t *testing.T) {
type args struct {
attr authorizer.Attributes
}
tests := []struct {
name string
args args
want *authorizationv1.ResourceAttributes
enableAuthorizationSelector bool
}{
{
name: "field selector: don't parse when disabled",
args: args{
attr: authorizer.AttributesRecord{
FieldSelectorRequirements: fields.Requirements{
fields.OneTermEqualSelector("foo", "bar").Requirements()[0],
},
FieldSelectorParsingErr: nil,
},
},
want: &authorizationv1.ResourceAttributes{},
},
{
name: "label selector: don't parse when disabled",
args: args{
attr: authorizer.AttributesRecord{
LabelSelectorRequirements: mustLabelRequirement("foo in (bar,baz)"),
LabelSelectorParsingErr: nil,
},
},
want: &authorizationv1.ResourceAttributes{},
},
{
name: "field selector: ignore error",
args: args{
attr: authorizer.AttributesRecord{
FieldSelectorRequirements: fields.Requirements{
fields.OneTermEqualSelector("foo", "bar").Requirements()[0],
},
FieldSelectorParsingErr: errors.New("failed"),
},
},
want: &authorizationv1.ResourceAttributes{
FieldSelector: &authorizationv1.FieldSelectorAttributes{
Requirements: []metav1.FieldSelectorRequirement{{Key: "foo", Operator: "In", Values: []string{"bar"}}},
},
},
enableAuthorizationSelector: true,
},
{
name: "label selector: ignore error",
args: args{
attr: authorizer.AttributesRecord{
LabelSelectorRequirements: mustLabelRequirement("foo in (bar,baz)"),
LabelSelectorParsingErr: errors.New("failed"),
},
},
want: &authorizationv1.ResourceAttributes{
LabelSelector: &authorizationv1.LabelSelectorAttributes{
Requirements: []metav1.LabelSelectorRequirement{{Key: "foo", Operator: "In", Values: []string{"bar", "baz"}}},
},
},
enableAuthorizationSelector: true,
},
{
name: "field selector: equals, double equals, in",
args: args{
attr: authorizer.AttributesRecord{
FieldSelectorRequirements: fields.Requirements{
{Operator: selection.Equals, Field: "foo", Value: "bar"},
{Operator: selection.DoubleEquals, Field: "one", Value: "two"},
{Operator: selection.In, Field: "apple", Value: "banana"},
},
FieldSelectorParsingErr: nil,
},
},
want: &authorizationv1.ResourceAttributes{
FieldSelector: &authorizationv1.FieldSelectorAttributes{
Requirements: []metav1.FieldSelectorRequirement{
{
Key: "foo",
Operator: "In",
Values: []string{"bar"},
},
{
Key: "one",
Operator: "In",
Values: []string{"two"},
},
{
Key: "apple",
Operator: "In",
Values: []string{"banana"},
},
},
},
},
enableAuthorizationSelector: true,
},
{
name: "field selector: not equals, not in",
args: args{
attr: authorizer.AttributesRecord{
FieldSelectorRequirements: fields.Requirements{
{Operator: selection.NotEquals, Field: "foo", Value: "bar"},
{Operator: selection.NotIn, Field: "apple", Value: "banana"},
},
FieldSelectorParsingErr: nil,
},
},
want: &authorizationv1.ResourceAttributes{
FieldSelector: &authorizationv1.FieldSelectorAttributes{
Requirements: []metav1.FieldSelectorRequirement{
{
Key: "foo",
Operator: "NotIn",
Values: []string{"bar"},
},
{
Key: "apple",
Operator: "NotIn",
Values: []string{"banana"},
},
},
},
},
enableAuthorizationSelector: true,
},
{
name: "field selector: unknown operator skipped",
args: args{
attr: authorizer.AttributesRecord{
FieldSelectorRequirements: fields.Requirements{
{Operator: selection.NotEquals, Field: "foo", Value: "bar"},
{Operator: selection.Operator("bad"), Field: "apple", Value: "banana"},
},
FieldSelectorParsingErr: nil,
},
},
want: &authorizationv1.ResourceAttributes{
FieldSelector: &authorizationv1.FieldSelectorAttributes{
Requirements: []metav1.FieldSelectorRequirement{
{
Key: "foo",
Operator: "NotIn",
Values: []string{"bar"},
},
},
},
},
enableAuthorizationSelector: true,
},
{
name: "field selector: no requirements has no fieldselector",
args: args{
attr: authorizer.AttributesRecord{
FieldSelectorRequirements: fields.Requirements{
{Operator: selection.Operator("bad"), Field: "apple", Value: "banana"},
},
FieldSelectorParsingErr: nil,
},
},
want: &authorizationv1.ResourceAttributes{},
enableAuthorizationSelector: true,
},
{
name: "label selector: in, equals, double equals",
args: args{
attr: authorizer.AttributesRecord{
LabelSelectorRequirements: mustLabelRequirement("foo in (bar,baz), one=two, apple==banana"),
LabelSelectorParsingErr: nil,
},
},
want: &authorizationv1.ResourceAttributes{
LabelSelector: &authorizationv1.LabelSelectorAttributes{
Requirements: []metav1.LabelSelectorRequirement{
{
Key: "apple",
Operator: "In",
Values: []string{"banana"},
},
{
Key: "foo",
Operator: "In",
Values: []string{"bar", "baz"},
},
{
Key: "one",
Operator: "In",
Values: []string{"two"},
},
},
},
},
enableAuthorizationSelector: true,
},
{
name: "label selector: not in, not equals",
args: args{
attr: authorizer.AttributesRecord{
LabelSelectorRequirements: mustLabelRequirement("foo notin (bar,baz), one!=two"),
LabelSelectorParsingErr: nil,
},
},
want: &authorizationv1.ResourceAttributes{
LabelSelector: &authorizationv1.LabelSelectorAttributes{
Requirements: []metav1.LabelSelectorRequirement{
{
Key: "foo",
Operator: "NotIn",
Values: []string{"bar", "baz"},
},
{
Key: "one",
Operator: "NotIn",
Values: []string{"two"},
},
},
},
},
enableAuthorizationSelector: true,
},
{
name: "label selector: exists, not exists",
args: args{
attr: authorizer.AttributesRecord{
LabelSelectorRequirements: mustLabelRequirement("foo, !one"),
LabelSelectorParsingErr: nil,
},
},
want: &authorizationv1.ResourceAttributes{
LabelSelector: &authorizationv1.LabelSelectorAttributes{
Requirements: []metav1.LabelSelectorRequirement{
{
Key: "foo",
Operator: "Exists",
},
{
Key: "one",
Operator: "DoesNotExist",
},
},
},
},
enableAuthorizationSelector: true,
},
{
name: "label selector: unknown operator skipped",
args: args{
attr: authorizer.AttributesRecord{
LabelSelectorRequirements: mustLabelRequirement("foo != bar, apple > 1"),
LabelSelectorParsingErr: nil,
},
},
want: &authorizationv1.ResourceAttributes{
LabelSelector: &authorizationv1.LabelSelectorAttributes{
Requirements: []metav1.LabelSelectorRequirement{
{
Key: "foo",
Operator: "NotIn",
Values: []string{"bar"},
},
},
},
},
enableAuthorizationSelector: true,
},
{
name: "label selector: no requirements has no labelselector",
args: args{
attr: authorizer.AttributesRecord{
LabelSelectorRequirements: mustLabelRequirement("apple > 1"),
LabelSelectorParsingErr: nil,
},
},
want: &authorizationv1.ResourceAttributes{},
enableAuthorizationSelector: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.enableAuthorizationSelector {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
}
if got := resourceAttributesFrom(tt.args.attr); !reflect.DeepEqual(got, tt.want) {
t.Errorf("resourceAttributesFrom() = %v, want %v", got, tt.want)
}
})
}
}