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
import (
"fmt"
apiequality "k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization"
)
@ -36,6 +39,7 @@ func ValidateSubjectAccessReviewSpec(spec authorizationapi.SubjectAccessReviewSp
if len(spec.User) == 0 && len(spec.Groups) == 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("user"), spec.User, `at least one of user or group must be specified`))
}
allErrs = append(allErrs, validateResourceAttributes(spec.ResourceAttributes, field.NewPath("spec.resourceAttributes"))...)
return allErrs
}
@ -50,6 +54,7 @@ func ValidateSelfSubjectAccessReviewSpec(spec authorizationapi.SelfSubjectAccess
if spec.ResourceAttributes == nil && spec.NonResourceAttributes == nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("resourceAttributes"), spec.NonResourceAttributes, `exactly one of nonResourceAttributes or resourceAttributes must be specified`))
}
allErrs = append(allErrs, validateResourceAttributes(spec.ResourceAttributes, field.NewPath("spec.resourceAttributes"))...)
return allErrs
}
@ -99,3 +104,59 @@ func ValidateLocalSubjectAccessReview(sar *authorizationapi.LocalSubjectAccessRe
return allErrs
}
func validateResourceAttributes(resourceAttributes *authorizationapi.ResourceAttributes, fldPath *field.Path) field.ErrorList {
if resourceAttributes == nil {
return nil
}
allErrs := field.ErrorList{}
allErrs = append(allErrs, validateFieldSelectorAttributes(resourceAttributes.FieldSelector, fldPath.Child("fieldSelector"))...)
allErrs = append(allErrs, validateLabelSelectorAttributes(resourceAttributes.LabelSelector, fldPath.Child("labelSelector"))...)
return allErrs
}
func validateFieldSelectorAttributes(selector *authorizationapi.FieldSelectorAttributes, fldPath *field.Path) field.ErrorList {
if selector == nil {
return nil
}
allErrs := field.ErrorList{}
if len(selector.RawSelector) > 0 && len(selector.Requirements) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("rawSelector"), selector.RawSelector, "may not specified at the same time as requirements"))
}
if len(selector.RawSelector) == 0 && len(selector.Requirements) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("requirements"), fmt.Sprintf("when %s is specified, requirements or rawSelector is required", fldPath)))
}
// AllowUnknownOperatorInRequirement enables *SubjectAccessReview requests from newer skewed clients which understand operators kube-apiserver does not know about to be authorized.
validationOptions := metav1validation.FieldSelectorValidationOptions{AllowUnknownOperatorInRequirement: true}
for i, requirement := range selector.Requirements {
allErrs = append(allErrs, metav1validation.ValidateFieldSelectorRequirement(requirement, validationOptions, fldPath.Child("requirements").Index(i))...)
}
return allErrs
}
func validateLabelSelectorAttributes(selector *authorizationapi.LabelSelectorAttributes, fldPath *field.Path) field.ErrorList {
if selector == nil {
return nil
}
allErrs := field.ErrorList{}
if len(selector.RawSelector) > 0 && len(selector.Requirements) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("rawSelector"), selector.RawSelector, "may not specified at the same time as requirements"))
}
if len(selector.RawSelector) == 0 && len(selector.Requirements) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("requirements"), fmt.Sprintf("when %s is specified, requirements or rawSelector is required", fldPath)))
}
// AllowUnknownOperatorInRequirement enables *SubjectAccessReview requests from newer skewed clients which understand operators kube-apiserver does not know about to be authorized.
validationOptions := metav1validation.LabelSelectorValidationOptions{AllowUnknownOperatorInRequirement: true}
for i, requirement := range selector.Requirements {
allErrs = append(allErrs, metav1validation.ValidateLabelSelectorRequirement(requirement, validationOptions, fldPath.Child("requirements").Index(i))...)
}
return allErrs
}

View File

@ -29,6 +29,50 @@ func TestValidateSARSpec(t *testing.T) {
successCases := []authorizationapi.SubjectAccessReviewSpec{
{ResourceAttributes: &authorizationapi.ResourceAttributes{}, User: "me"},
{NonResourceAttributes: &authorizationapi.NonResourceAttributes{}, Groups: []string{"my-group"}},
{ // field raw selector
User: "me",
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{
RawSelector: "***foo",
},
},
},
{ // label raw selector
User: "me",
ResourceAttributes: &authorizationapi.ResourceAttributes{
LabelSelector: &authorizationapi.LabelSelectorAttributes{
RawSelector: "***foo",
},
},
},
{ // unknown field operator
User: "me",
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{
Requirements: []metav1.FieldSelectorRequirement{
{
Key: "k",
Operator: metav1.FieldSelectorOperator("fake"),
Values: []string{"val"},
},
},
},
},
},
{ // unknown label operator
User: "me",
ResourceAttributes: &authorizationapi.ResourceAttributes{
LabelSelector: &authorizationapi.LabelSelectorAttributes{
Requirements: []metav1.LabelSelectorRequirement{
{
Key: "k",
Operator: metav1.LabelSelectorOperator("fake"),
Values: []string{"val"},
},
},
},
},
},
}
for _, successCase := range successCases {
if errs := ValidateSubjectAccessReviewSpec(successCase, field.NewPath("spec")); len(errs) != 0 {
@ -58,29 +102,257 @@ func TestValidateSARSpec(t *testing.T) {
ResourceAttributes: &authorizationapi.ResourceAttributes{},
},
msg: `spec.user: Invalid value: "": at least one of user or group must be specified`,
}, {
name: "resource attributes: field selector specify both",
obj: authorizationapi.SubjectAccessReviewSpec{
User: "me",
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{
RawSelector: "foo",
Requirements: []metav1.FieldSelectorRequirement{
{},
},
},
},
},
msg: `spec.resourceAttributes.fieldSelector.rawSelector: Invalid value: "foo": may not specified at the same time as requirements`,
}, {
name: "resource attributes: field selector specify neither",
obj: authorizationapi.SubjectAccessReviewSpec{
User: "me",
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{},
},
},
msg: `spec.resourceAttributes.fieldSelector.requirements: Required value: when spec.resourceAttributes.fieldSelector is specified, requirements or rawSelector is required`,
}, {
name: "resource attributes: field selector no key",
obj: authorizationapi.SubjectAccessReviewSpec{
User: "me",
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{
Requirements: []metav1.FieldSelectorRequirement{
{
Key: "",
},
},
},
},
},
msg: `spec.resourceAttributes.fieldSelector.requirements[0].key: Required value: must be specified`,
}, {
name: "resource attributes: field selector no value for in",
obj: authorizationapi.SubjectAccessReviewSpec{
User: "me",
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{
Requirements: []metav1.FieldSelectorRequirement{
{
Key: "k",
Operator: metav1.FieldSelectorOpIn,
Values: []string{},
},
},
},
},
},
msg: "spec.resourceAttributes.fieldSelector.requirements[0].values: Required value: must be specified when `operator` is 'In' or 'NotIn'",
}, {
name: "resource attributes: field selector no value for not in",
obj: authorizationapi.SubjectAccessReviewSpec{
User: "me",
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{
Requirements: []metav1.FieldSelectorRequirement{
{
Key: "k",
Operator: metav1.FieldSelectorOpNotIn,
Values: []string{},
},
},
},
},
},
msg: "spec.resourceAttributes.fieldSelector.requirements[0].values: Required value: must be specified when `operator` is 'In' or 'NotIn'",
}, {
name: "resource attributes: field selector values for exists",
obj: authorizationapi.SubjectAccessReviewSpec{
User: "me",
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{
Requirements: []metav1.FieldSelectorRequirement{
{
Key: "k",
Operator: metav1.FieldSelectorOpExists,
Values: []string{"val"},
},
},
},
},
},
msg: "spec.resourceAttributes.fieldSelector.requirements[0].values: Forbidden: may not be specified when `operator` is 'Exists' or 'DoesNotExist'",
}, {
name: "resource attributes: field selector values for not exists",
obj: authorizationapi.SubjectAccessReviewSpec{
User: "me",
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{
Requirements: []metav1.FieldSelectorRequirement{
{
Key: "k",
Operator: metav1.FieldSelectorOpDoesNotExist,
Values: []string{"val"},
},
},
},
},
},
msg: "spec.resourceAttributes.fieldSelector.requirements[0].values: Forbidden: may not be specified when `operator` is 'Exists' or 'DoesNotExist'",
}, {
name: "resource attributes: label selector specify both",
obj: authorizationapi.SubjectAccessReviewSpec{
User: "me",
ResourceAttributes: &authorizationapi.ResourceAttributes{
LabelSelector: &authorizationapi.LabelSelectorAttributes{
RawSelector: "foo",
Requirements: []metav1.LabelSelectorRequirement{
{},
},
},
},
},
msg: `spec.resourceAttributes.labelSelector.rawSelector: Invalid value: "foo": may not specified at the same time as requirements`,
}, {
name: "resource attributes: label selector specify neither",
obj: authorizationapi.SubjectAccessReviewSpec{
User: "me",
ResourceAttributes: &authorizationapi.ResourceAttributes{
LabelSelector: &authorizationapi.LabelSelectorAttributes{},
},
},
msg: `spec.resourceAttributes.labelSelector.requirements: Required value: when spec.resourceAttributes.labelSelector is specified, requirements or rawSelector is required`,
}, {
name: "resource attributes: label selector no key",
obj: authorizationapi.SubjectAccessReviewSpec{
User: "me",
ResourceAttributes: &authorizationapi.ResourceAttributes{
LabelSelector: &authorizationapi.LabelSelectorAttributes{
Requirements: []metav1.LabelSelectorRequirement{
{
Key: "",
},
},
},
},
},
msg: `spec.resourceAttributes.labelSelector.requirements[0].key: Invalid value: "": name part must be non-empty`,
}, {
name: "resource attributes: label selector invalid label name",
obj: authorizationapi.SubjectAccessReviewSpec{
User: "me",
ResourceAttributes: &authorizationapi.ResourceAttributes{
LabelSelector: &authorizationapi.LabelSelectorAttributes{
Requirements: []metav1.LabelSelectorRequirement{
{
Key: "()foo",
},
},
},
},
},
msg: `spec.resourceAttributes.labelSelector.requirements[0].key: Invalid value: "()foo": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')`,
}, {
name: "resource attributes: label selector no value for in",
obj: authorizationapi.SubjectAccessReviewSpec{
User: "me",
ResourceAttributes: &authorizationapi.ResourceAttributes{
LabelSelector: &authorizationapi.LabelSelectorAttributes{
Requirements: []metav1.LabelSelectorRequirement{
{
Key: "k",
Operator: metav1.LabelSelectorOpIn,
Values: []string{},
},
},
},
},
},
msg: "spec.resourceAttributes.labelSelector.requirements[0].values: Required value: must be specified when `operator` is 'In' or 'NotIn'",
}, {
name: "resource attributes: label selector no value for not in",
obj: authorizationapi.SubjectAccessReviewSpec{
User: "me",
ResourceAttributes: &authorizationapi.ResourceAttributes{
LabelSelector: &authorizationapi.LabelSelectorAttributes{
Requirements: []metav1.LabelSelectorRequirement{
{
Key: "k",
Operator: metav1.LabelSelectorOpNotIn,
Values: []string{},
},
},
},
},
},
msg: "spec.resourceAttributes.labelSelector.requirements[0].values: Required value: must be specified when `operator` is 'In' or 'NotIn'",
}, {
name: "resource attributes: label selector values for exists",
obj: authorizationapi.SubjectAccessReviewSpec{
User: "me",
ResourceAttributes: &authorizationapi.ResourceAttributes{
LabelSelector: &authorizationapi.LabelSelectorAttributes{
Requirements: []metav1.LabelSelectorRequirement{
{
Key: "k",
Operator: metav1.LabelSelectorOpExists,
Values: []string{"val"},
},
},
},
},
},
msg: "spec.resourceAttributes.labelSelector.requirements[0].values: Forbidden: may not be specified when `operator` is 'Exists' or 'DoesNotExist'",
}, {
name: "resource attributes: label selector values for not exists",
obj: authorizationapi.SubjectAccessReviewSpec{
User: "me",
ResourceAttributes: &authorizationapi.ResourceAttributes{
LabelSelector: &authorizationapi.LabelSelectorAttributes{
Requirements: []metav1.LabelSelectorRequirement{
{
Key: "k",
Operator: metav1.LabelSelectorOpDoesNotExist,
Values: []string{"val"},
},
},
},
},
},
msg: "spec.resourceAttributes.labelSelector.requirements[0].values: Forbidden: may not be specified when `operator` is 'Exists' or 'DoesNotExist'",
}}
for _, c := range errorCases {
errs := ValidateSubjectAccessReviewSpec(c.obj, field.NewPath("spec"))
if len(errs) == 0 {
t.Errorf("%s: expected failure for %q", c.name, 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)
}
errs = ValidateSubjectAccessReview(&authorizationapi.SubjectAccessReview{Spec: c.obj})
if len(errs) == 0 {
t.Errorf("%s: expected failure for %q", c.name, 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)
}
errs = ValidateLocalSubjectAccessReview(&authorizationapi.LocalSubjectAccessReview{Spec: c.obj})
if len(errs) == 0 {
t.Errorf("%s: expected failure for %q", c.name, 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.Run(c.name, func(t *testing.T) {
errs := ValidateSubjectAccessReviewSpec(c.obj, field.NewPath("spec"))
if len(errs) == 0 {
t.Errorf("%s: expected failure for %q", c.name, c.msg)
} else if !strings.Contains(errs[0].Error(), c.msg) {
t.Errorf("%s: unexpected error: %q, expected: %q", c.name, errs[0], c.msg)
}
errs = ValidateSubjectAccessReview(&authorizationapi.SubjectAccessReview{Spec: c.obj})
if len(errs) == 0 {
t.Errorf("%s: expected failure for %q", c.name, 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)
}
errs = ValidateLocalSubjectAccessReview(&authorizationapi.LocalSubjectAccessReview{Spec: c.obj})
if len(errs) == 0 {
t.Errorf("%s: expected failure for %q", c.name, 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)
}
})
}
}
@ -109,6 +381,20 @@ func TestValidateSelfSAR(t *testing.T) {
NonResourceAttributes: &authorizationapi.NonResourceAttributes{},
},
msg: "cannot be specified in combination with resourceAttributes",
}, {
// here we only test one to be sure the function is called. The more exhaustive suite is tested above.
name: "resource attributes: label selector specify both",
obj: authorizationapi.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
LabelSelector: &authorizationapi.LabelSelectorAttributes{
RawSelector: "foo",
Requirements: []metav1.LabelSelectorRequirement{
{},
},
},
},
},
msg: `spec.resourceAttributes.labelSelector.rawSelector: Invalid value: "foo": may not specified at the same time as requirements`,
}}
for _, c := range errorCases {
@ -175,6 +461,23 @@ func TestValidateLocalSAR(t *testing.T) {
},
},
msg: "disallowed on this kind of request",
}, {
// here we only test one to be sure the function is called. The more exhaustive suite is tested above.
name: "resource attributes: label selector specify both",
obj: &authorizationapi.LocalSubjectAccessReview{
Spec: authorizationapi.SubjectAccessReviewSpec{
User: "user",
ResourceAttributes: &authorizationapi.ResourceAttributes{
LabelSelector: &authorizationapi.LabelSelectorAttributes{
RawSelector: "foo",
Requirements: []metav1.LabelSelectorRequirement{
{},
},
},
},
},
},
msg: `spec.resourceAttributes.labelSelector.rawSelector: Invalid value: "foo": may not specified at the same time as requirements`,
}}
for _, c := range errorCases {

View File

@ -55,23 +55,25 @@ func TestGroupVersions(t *testing.T) {
// but are also registered in internal versions (or referenced from internal types),
// so we explicitly allow tags for them
var typesAllowedTags = map[reflect.Type]bool{
reflect.TypeOf(intstr.IntOrString{}): true,
reflect.TypeOf(metav1.Time{}): true,
reflect.TypeOf(metav1.MicroTime{}): true,
reflect.TypeOf(metav1.Duration{}): true,
reflect.TypeOf(metav1.TypeMeta{}): true,
reflect.TypeOf(metav1.ListMeta{}): true,
reflect.TypeOf(metav1.ObjectMeta{}): true,
reflect.TypeOf(metav1.OwnerReference{}): true,
reflect.TypeOf(metav1.LabelSelector{}): true,
reflect.TypeOf(metav1.GetOptions{}): true,
reflect.TypeOf(metav1.ListOptions{}): true,
reflect.TypeOf(metav1.DeleteOptions{}): true,
reflect.TypeOf(metav1.GroupVersionKind{}): true,
reflect.TypeOf(metav1.GroupVersionResource{}): true,
reflect.TypeOf(metav1.Status{}): true,
reflect.TypeOf(metav1.Condition{}): true,
reflect.TypeOf(runtime.RawExtension{}): true,
reflect.TypeOf(intstr.IntOrString{}): true,
reflect.TypeOf(metav1.Time{}): true,
reflect.TypeOf(metav1.MicroTime{}): true,
reflect.TypeOf(metav1.Duration{}): true,
reflect.TypeOf(metav1.TypeMeta{}): true,
reflect.TypeOf(metav1.ListMeta{}): true,
reflect.TypeOf(metav1.ObjectMeta{}): true,
reflect.TypeOf(metav1.OwnerReference{}): true,
reflect.TypeOf(metav1.LabelSelector{}): true,
reflect.TypeOf(metav1.LabelSelectorRequirement{}): true,
reflect.TypeOf(metav1.FieldSelectorRequirement{}): true,
reflect.TypeOf(metav1.GetOptions{}): true,
reflect.TypeOf(metav1.ListOptions{}): true,
reflect.TypeOf(metav1.DeleteOptions{}): true,
reflect.TypeOf(metav1.GroupVersionKind{}): true,
reflect.TypeOf(metav1.GroupVersionResource{}): true,
reflect.TypeOf(metav1.Status{}): true,
reflect.TypeOf(metav1.Condition{}): true,
reflect.TypeOf(runtime.RawExtension{}): true,
}
// These fields are limited exceptions to the standard JSON naming structure.

View File

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

View File

@ -25,7 +25,9 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
genericfeatures "k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/registry/rest"
utilfeature "k8s.io/apiserver/pkg/util/feature"
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization"
authorizationvalidation "k8s.io/kubernetes/pkg/apis/authorization/validation"
authorizationutil "k8s.io/kubernetes/pkg/registry/authorization/util"
@ -64,6 +66,14 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("not a LocaLocalSubjectAccessReview: %#v", obj))
}
// clear fields if the featuregate is disabled
if !utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) {
if localSubjectAccessReview.Spec.ResourceAttributes != nil {
localSubjectAccessReview.Spec.ResourceAttributes.FieldSelector = nil
localSubjectAccessReview.Spec.ResourceAttributes.LabelSelector = nil
}
}
if errs := authorizationvalidation.ValidateLocalSubjectAccessReview(localSubjectAccessReview); len(errs) > 0 {
return nil, apierrors.NewInvalid(authorizationapi.Kind(localSubjectAccessReview.Kind), "", errs)
}
@ -89,9 +99,7 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation
Denied: (decision == authorizer.DecisionDeny),
Reason: reason,
}
if evaluationErr != nil {
localSubjectAccessReview.Status.EvaluationError = evaluationErr.Error()
}
localSubjectAccessReview.Status.EvaluationError = authorizationutil.BuildEvaluationError(evaluationErr, authorizationAttributes)
return localSubjectAccessReview, nil
}

View File

@ -25,7 +25,9 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
genericfeatures "k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/registry/rest"
utilfeature "k8s.io/apiserver/pkg/util/feature"
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization"
authorizationvalidation "k8s.io/kubernetes/pkg/apis/authorization/validation"
authorizationutil "k8s.io/kubernetes/pkg/registry/authorization/util"
@ -64,6 +66,13 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("not a SelfSubjectAccessReview: %#v", obj))
}
// clear fields if the featuregate is disabled
if !utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) {
if selfSAR.Spec.ResourceAttributes != nil {
selfSAR.Spec.ResourceAttributes.FieldSelector = nil
selfSAR.Spec.ResourceAttributes.LabelSelector = nil
}
}
if errs := authorizationvalidation.ValidateSelfSubjectAccessReview(selfSAR); len(errs) > 0 {
return nil, apierrors.NewInvalid(authorizationapi.Kind(selfSAR.Kind), "", errs)
}
@ -92,9 +101,7 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation
Denied: (decision == authorizer.DecisionDeny),
Reason: reason,
}
if evaluationErr != nil {
selfSAR.Status.EvaluationError = evaluationErr.Error()
}
selfSAR.Status.EvaluationError = authorizationutil.BuildEvaluationError(evaluationErr, authorizationAttributes)
return selfSAR, nil
}

View File

@ -24,7 +24,9 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericfeatures "k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/registry/rest"
utilfeature "k8s.io/apiserver/pkg/util/feature"
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization"
authorizationvalidation "k8s.io/kubernetes/pkg/apis/authorization/validation"
authorizationutil "k8s.io/kubernetes/pkg/registry/authorization/util"
@ -63,6 +65,13 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("not a SubjectAccessReview: %#v", obj))
}
// clear fields if the featuregate is disabled
if !utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) {
if subjectAccessReview.Spec.ResourceAttributes != nil {
subjectAccessReview.Spec.ResourceAttributes.FieldSelector = nil
subjectAccessReview.Spec.ResourceAttributes.LabelSelector = nil
}
}
if errs := authorizationvalidation.ValidateSubjectAccessReview(subjectAccessReview); len(errs) > 0 {
return nil, apierrors.NewInvalid(authorizationapi.Kind(subjectAccessReview.Kind), "", errs)
}
@ -81,9 +90,7 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation
Denied: (decision == authorizer.DecisionDeny),
Reason: reason,
}
if evaluationErr != nil {
subjectAccessReview.Status.EvaluationError = evaluationErr.Error()
}
subjectAccessReview.Status.EvaluationError = authorizationutil.BuildEvaluationError(evaluationErr, authorizationAttributes)
return subjectAccessReview, nil
}

View File

@ -24,10 +24,15 @@ import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
genericfeatures "k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/registry/rest"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization"
)
@ -187,8 +192,52 @@ func TestCreate(t *testing.T) {
Denied: true,
},
},
"resource denied, valid selectors": {
spec: authorizationapi.SubjectAccessReviewSpec{
User: "bob",
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{RawSelector: "foo=bar"},
LabelSelector: &authorizationapi.LabelSelectorAttributes{RawSelector: "key=value"},
},
},
decision: authorizer.DecisionDeny,
expectedAttrs: authorizer.AttributesRecord{
User: &user.DefaultInfo{Name: "bob"},
ResourceRequest: true,
APIVersion: "*",
FieldSelectorRequirements: fields.Requirements{{Operator: "=", Field: "foo", Value: "bar"}},
LabelSelectorRequirements: mustParse("key=value"),
},
expectedStatus: authorizationapi.SubjectAccessReviewStatus{
Allowed: false,
Denied: true,
},
},
"resource denied, invalid selectors": {
spec: authorizationapi.SubjectAccessReviewSpec{
User: "bob",
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{RawSelector: "key in value"},
LabelSelector: &authorizationapi.LabelSelectorAttributes{RawSelector: "&"},
},
},
decision: authorizer.DecisionDeny,
expectedAttrs: authorizer.AttributesRecord{
User: &user.DefaultInfo{Name: "bob"},
ResourceRequest: true,
APIVersion: "*",
},
expectedStatus: authorizationapi.SubjectAccessReviewStatus{
Allowed: false,
Denied: true,
EvaluationError: `spec.resourceAttributes.fieldSelector ignored due to parse error; spec.resourceAttributes.labelSelector ignored due to parse error`,
},
},
}
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
for k, tc := range testcases {
auth := &fakeAuthorizer{
decision: tc.decision,
@ -208,8 +257,13 @@ func TestCreate(t *testing.T) {
}
continue
}
if !reflect.DeepEqual(auth.attrs, tc.expectedAttrs) {
t.Errorf("%s: expected\n%#v\ngot\n%#v", k, tc.expectedAttrs, auth.attrs)
gotAttrs := auth.attrs.(authorizer.AttributesRecord)
if tc.expectedStatus.EvaluationError != "" {
gotAttrs.FieldSelectorParsingErr = nil
gotAttrs.LabelSelectorParsingErr = nil
}
if !reflect.DeepEqual(gotAttrs, tc.expectedAttrs) {
t.Errorf("%s: expected\n%#v\ngot\n%#v", k, tc.expectedAttrs, gotAttrs)
}
status := result.(*authorizationapi.SubjectAccessReview).Status
if !reflect.DeepEqual(status, tc.expectedStatus) {
@ -217,3 +271,12 @@ func TestCreate(t *testing.T) {
}
}
}
func mustParse(s string) labels.Requirements {
selector, err := labels.Parse(s)
if err != nil {
panic(err)
}
reqs, _ := selector.Requirements()
return reqs
}

View File

@ -17,14 +17,24 @@ limitations under the License.
package util
import (
"fmt"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization"
)
// ResourceAttributesFrom combines the API object information and the user.Info from the context to build a full authorizer.AttributesRecord for resource access
func ResourceAttributesFrom(user user.Info, in authorizationapi.ResourceAttributes) authorizer.AttributesRecord {
return authorizer.AttributesRecord{
ret := authorizer.AttributesRecord{
User: user,
Verb: in.Verb,
Namespace: in.Namespace,
@ -35,6 +45,129 @@ func ResourceAttributesFrom(user user.Info, in authorizationapi.ResourceAttribut
Name: in.Name,
ResourceRequest: true,
}
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) {
if in.LabelSelector != nil {
if len(in.LabelSelector.RawSelector) > 0 {
labelSelector, err := labels.Parse(in.LabelSelector.RawSelector)
if err != nil {
ret.LabelSelectorRequirements, ret.LabelSelectorParsingErr = nil, err
} else {
requirements, _ /*selectable*/ := labelSelector.Requirements()
ret.LabelSelectorRequirements, ret.LabelSelectorParsingErr = requirements, nil
}
}
if len(in.LabelSelector.Requirements) > 0 {
ret.LabelSelectorRequirements, ret.LabelSelectorParsingErr = labelSelectorAsSelector(in.LabelSelector.Requirements)
}
}
if in.FieldSelector != nil {
if len(in.FieldSelector.RawSelector) > 0 {
fieldSelector, err := fields.ParseSelector(in.FieldSelector.RawSelector)
if err != nil {
ret.FieldSelectorRequirements, ret.FieldSelectorParsingErr = nil, err
} else {
ret.FieldSelectorRequirements, ret.FieldSelectorParsingErr = fieldSelector.Requirements(), nil
}
}
if len(in.FieldSelector.Requirements) > 0 {
ret.FieldSelectorRequirements, ret.FieldSelectorParsingErr = fieldSelectorAsSelector(in.FieldSelector.Requirements)
}
}
}
return ret
}
var labelSelectorOpToSelectionOp = map[metav1.LabelSelectorOperator]selection.Operator{
metav1.LabelSelectorOpIn: selection.In,
metav1.LabelSelectorOpNotIn: selection.NotIn,
metav1.LabelSelectorOpExists: selection.Exists,
metav1.LabelSelectorOpDoesNotExist: selection.DoesNotExist,
}
func labelSelectorAsSelector(requirements []metav1.LabelSelectorRequirement) (labels.Requirements, error) {
if len(requirements) == 0 {
return nil, nil
}
reqs := make([]labels.Requirement, 0, len(requirements))
var errs []error
for _, expr := range requirements {
op, ok := labelSelectorOpToSelectionOp[expr.Operator]
if !ok {
errs = append(errs, fmt.Errorf("%q is not a valid label selector operator", expr.Operator))
continue
}
values := expr.Values
if len(values) == 0 {
values = nil
}
req, err := labels.NewRequirement(expr.Key, op, values)
if err != nil {
errs = append(errs, err)
continue
}
reqs = append(reqs, *req)
}
// If this happens, it means all requirements ended up getting skipped.
// Return nil rather than [].
if len(reqs) == 0 {
reqs = nil
}
// Return any accumulated errors along with any accumulated requirements, so recognized / valid requirements can be considered by authorization.
// This is safe because requirements are ANDed together so dropping unknown / invalid ones results in a strictly broader authorization check.
return labels.Requirements(reqs), utilerrors.NewAggregate(errs)
}
func fieldSelectorAsSelector(requirements []metav1.FieldSelectorRequirement) (fields.Requirements, error) {
if len(requirements) == 0 {
return nil, nil
}
reqs := make([]fields.Requirement, 0, len(requirements))
var errs []error
for _, expr := range requirements {
if len(expr.Values) > 1 {
errs = append(errs, fmt.Errorf("fieldSelectors do not yet support multiple values"))
continue
}
switch expr.Operator {
case metav1.FieldSelectorOpIn:
if len(expr.Values) != 1 {
errs = append(errs, fmt.Errorf("fieldSelectors in must have one value"))
continue
}
// when converting to fields.Requirement, use Equals to match how parsed field selectors behave
reqs = append(reqs, fields.Requirement{Field: expr.Key, Operator: selection.Equals, Value: expr.Values[0]})
case metav1.FieldSelectorOpNotIn:
if len(expr.Values) != 1 {
errs = append(errs, fmt.Errorf("fieldSelectors not in must have one value"))
continue
}
// when converting to fields.Requirement, use NotEquals to match how parsed field selectors behave
reqs = append(reqs, fields.Requirement{Field: expr.Key, Operator: selection.NotEquals, Value: expr.Values[0]})
case metav1.FieldSelectorOpExists, metav1.FieldSelectorOpDoesNotExist:
errs = append(errs, fmt.Errorf("fieldSelectors do not yet support %v", expr.Operator))
continue
default:
errs = append(errs, fmt.Errorf("%q is not a valid field selector operator", expr.Operator))
continue
}
}
// If this happens, it means all requirements ended up getting skipped.
// Return nil rather than [].
if len(reqs) == 0 {
reqs = nil
}
// Return any accumulated errors along with any accumulated requirements, so recognized / valid requirements can be considered by authorization.
// This is safe because requirements are ANDed together so dropping unknown / invalid ones results in a strictly broader authorization check.
return fields.Requirements(reqs), utilerrors.NewAggregate(errs)
}
// NonResourceAttributesFrom combines the API object information and the user.Info from the context to build a full authorizer.AttributesRecord for non resource access
@ -85,3 +218,27 @@ func matchAllVersionIfEmpty(version string) string {
}
return version
}
// BuildEvaluationError constructs the evaluation error string to include in *SubjectAccessReview status
// based on the authorizer evaluation error and any field and label selector parse errors.
func BuildEvaluationError(evaluationError error, attrs authorizer.AttributesRecord) string {
var evaluationErrors []string
if evaluationError != nil {
evaluationErrors = append(evaluationErrors, evaluationError.Error())
}
if reqs, err := attrs.GetFieldSelector(); err != nil {
if len(reqs) > 0 {
evaluationErrors = append(evaluationErrors, "spec.resourceAttributes.fieldSelector partially ignored due to parse error")
} else {
evaluationErrors = append(evaluationErrors, "spec.resourceAttributes.fieldSelector ignored due to parse error")
}
}
if reqs, err := attrs.GetLabelSelector(); err != nil {
if len(reqs) > 0 {
evaluationErrors = append(evaluationErrors, "spec.resourceAttributes.labelSelector partially ignored due to parse error")
} else {
evaluationErrors = append(evaluationErrors, "spec.resourceAttributes.labelSelector ignored due to parse error")
}
}
return strings.Join(evaluationErrors, "; ")
}

View File

@ -17,12 +17,21 @@ limitations under the License.
package util
import (
"errors"
"reflect"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization"
)
@ -37,6 +46,10 @@ func TestResourceAttributesFrom(t *testing.T) {
"Subresource",
"Name",
// Fields we read and parse in ResourceAttributesFrom
"FieldSelector",
"LabelSelector",
// Fields we copy in NonResourceAttributesFrom
"Path",
"Verb",
@ -60,6 +73,12 @@ func TestResourceAttributesFrom(t *testing.T) {
"Name",
"ResourceRequest",
// Fields we compute and set in ResourceAttributesFrom
"FieldSelectorRequirements",
"FieldSelectorParsingErr",
"LabelSelectorRequirements",
"LabelSelectorParsingErr",
// Fields we set in NonResourceAttributesFrom
"User",
"ResourceRequest",
@ -75,13 +94,22 @@ func TestResourceAttributesFrom(t *testing.T) {
}
func TestAuthorizationAttributesFrom(t *testing.T) {
mustRequirement := func(key string, op selection.Operator, vals []string) labels.Requirement {
ret, err := labels.NewRequirement(key, op, vals)
if err != nil {
panic(err)
}
return *ret
}
type args struct {
spec authorizationapi.SubjectAccessReviewSpec
}
tests := []struct {
name string
args args
want authorizer.AttributesRecord
name string
args args
want authorizer.AttributesRecord
enableAuthorizationSelector bool
}{
{
name: "nonresource",
@ -162,11 +190,461 @@ func TestAuthorizationAttributesFrom(t *testing.T) {
ResourceRequest: true,
},
},
{
name: "field: ignore when featuregate off",
args: args{
spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{
RawSelector: "foo=bar",
},
},
},
},
want: authorizer.AttributesRecord{
User: &user.DefaultInfo{},
ResourceRequest: true,
APIVersion: "*",
},
},
{
name: "field: raw selector",
args: args{
spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{
RawSelector: "foo=bar",
},
},
},
},
want: authorizer.AttributesRecord{
User: &user.DefaultInfo{},
ResourceRequest: true,
APIVersion: "*",
FieldSelectorRequirements: fields.Requirements{
{Operator: "=", Field: "foo", Value: "bar"},
},
},
enableAuthorizationSelector: true,
},
{
name: "field: raw selector error",
args: args{
spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{
RawSelector: "&foo",
},
},
},
},
want: authorizer.AttributesRecord{
User: &user.DefaultInfo{},
ResourceRequest: true,
APIVersion: "*",
FieldSelectorParsingErr: errors.New("invalid selector: '&foo'; can't understand '&foo'"),
},
enableAuthorizationSelector: true,
},
{
name: "field: requirements",
args: args{
spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{
Requirements: []metav1.FieldSelectorRequirement{
{
Key: "one",
Operator: "In",
Values: []string{"apple"},
},
{
Key: "two",
Operator: "NotIn",
Values: []string{"banana"},
},
},
},
},
},
},
want: authorizer.AttributesRecord{
User: &user.DefaultInfo{},
ResourceRequest: true,
APIVersion: "*",
FieldSelectorRequirements: fields.Requirements{
{Operator: "=", Field: "one", Value: "apple"},
{Operator: "!=", Field: "two", Value: "banana"},
},
},
enableAuthorizationSelector: true,
},
{
name: "field: requirements too many values",
args: args{
spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{
Requirements: []metav1.FieldSelectorRequirement{
{
Key: "one",
Operator: "In",
Values: []string{"apple", "other"},
},
},
},
},
},
},
want: authorizer.AttributesRecord{
User: &user.DefaultInfo{},
ResourceRequest: true,
APIVersion: "*",
FieldSelectorParsingErr: utilerrors.NewAggregate([]error{errors.New("fieldSelectors do not yet support multiple values")}),
},
enableAuthorizationSelector: true,
},
{
name: "field: requirements missing in value",
args: args{
spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{
Requirements: []metav1.FieldSelectorRequirement{
{
Key: "one",
Operator: "In",
Values: []string{},
},
},
},
},
},
},
want: authorizer.AttributesRecord{
User: &user.DefaultInfo{},
ResourceRequest: true,
APIVersion: "*",
FieldSelectorParsingErr: utilerrors.NewAggregate([]error{errors.New("fieldSelectors in must have one value")}),
},
enableAuthorizationSelector: true,
},
{
name: "field: requirements missing notin value",
args: args{
spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{
Requirements: []metav1.FieldSelectorRequirement{
{
Key: "one",
Operator: "NotIn",
Values: []string{},
},
},
},
},
},
},
want: authorizer.AttributesRecord{
User: &user.DefaultInfo{},
ResourceRequest: true,
APIVersion: "*",
FieldSelectorParsingErr: utilerrors.NewAggregate([]error{errors.New("fieldSelectors not in must have one value")}),
},
enableAuthorizationSelector: true,
},
{
name: "field: requirements exists",
args: args{
spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{
Requirements: []metav1.FieldSelectorRequirement{
{
Key: "one",
Operator: "Exists",
Values: []string{},
},
},
},
},
},
},
want: authorizer.AttributesRecord{
User: &user.DefaultInfo{},
ResourceRequest: true,
APIVersion: "*",
FieldSelectorParsingErr: utilerrors.NewAggregate([]error{errors.New("fieldSelectors do not yet support Exists")}),
},
enableAuthorizationSelector: true,
},
{
name: "field: requirements DoesNotExist",
args: args{
spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{
Requirements: []metav1.FieldSelectorRequirement{
{
Key: "one",
Operator: "DoesNotExist",
Values: []string{},
},
},
},
},
},
},
want: authorizer.AttributesRecord{
User: &user.DefaultInfo{},
ResourceRequest: true,
APIVersion: "*",
FieldSelectorParsingErr: utilerrors.NewAggregate([]error{errors.New("fieldSelectors do not yet support DoesNotExist")}),
},
enableAuthorizationSelector: true,
},
{
name: "field: requirements bad operator",
args: args{
spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
FieldSelector: &authorizationapi.FieldSelectorAttributes{
Requirements: []metav1.FieldSelectorRequirement{
{
Key: "one",
Operator: "bad",
Values: []string{},
},
},
},
},
},
},
want: authorizer.AttributesRecord{
User: &user.DefaultInfo{},
ResourceRequest: true,
APIVersion: "*",
FieldSelectorParsingErr: utilerrors.NewAggregate([]error{errors.New("\"bad\" is not a valid field selector operator")}),
},
enableAuthorizationSelector: true,
},
{
name: "label: ignore when featuregate off",
args: args{
spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
LabelSelector: &authorizationapi.LabelSelectorAttributes{
RawSelector: "foo=bar",
},
},
},
},
want: authorizer.AttributesRecord{
User: &user.DefaultInfo{},
ResourceRequest: true,
APIVersion: "*",
},
},
{
name: "label: raw selector",
args: args{
spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
LabelSelector: &authorizationapi.LabelSelectorAttributes{
RawSelector: "foo=bar",
},
},
},
},
want: authorizer.AttributesRecord{
User: &user.DefaultInfo{},
ResourceRequest: true,
APIVersion: "*",
LabelSelectorRequirements: labels.Requirements{
mustRequirement("foo", "=", []string{"bar"}),
},
},
enableAuthorizationSelector: true,
},
{
name: "label: raw selector error",
args: args{
spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
LabelSelector: &authorizationapi.LabelSelectorAttributes{
RawSelector: "&foo",
},
},
},
},
want: authorizer.AttributesRecord{
User: &user.DefaultInfo{},
ResourceRequest: true,
APIVersion: "*",
LabelSelectorParsingErr: errors.New("unable to parse requirement: <nil>: Invalid value: \"&foo\": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')"),
},
enableAuthorizationSelector: true,
},
{
name: "label: requirements",
args: args{
spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
LabelSelector: &authorizationapi.LabelSelectorAttributes{
Requirements: []metav1.LabelSelectorRequirement{
{
Key: "one",
Operator: "In",
Values: []string{"apple"},
},
{
Key: "two",
Operator: "NotIn",
Values: []string{"banana"},
},
},
},
},
},
},
want: authorizer.AttributesRecord{
User: &user.DefaultInfo{},
ResourceRequest: true,
APIVersion: "*",
LabelSelectorRequirements: labels.Requirements{
mustRequirement("one", "in", []string{"apple"}),
mustRequirement("two", "notin", []string{"banana"}),
},
},
enableAuthorizationSelector: true,
},
{
name: "label: requirements multiple values",
args: args{
spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
LabelSelector: &authorizationapi.LabelSelectorAttributes{
Requirements: []metav1.LabelSelectorRequirement{
{
Key: "one",
Operator: "In",
Values: []string{"apple", "other"},
},
{
Key: "two",
Operator: "NotIn",
Values: []string{"carrot", "donut"},
},
},
},
},
},
},
want: authorizer.AttributesRecord{
User: &user.DefaultInfo{},
ResourceRequest: true,
APIVersion: "*",
LabelSelectorRequirements: labels.Requirements{
mustRequirement("one", "in", []string{"apple", "other"}),
mustRequirement("two", "notin", []string{"carrot", "donut"}),
},
},
enableAuthorizationSelector: true,
},
{
name: "label: requirements exists",
args: args{
spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
LabelSelector: &authorizationapi.LabelSelectorAttributes{
Requirements: []metav1.LabelSelectorRequirement{
{
Key: "one",
Operator: "Exists",
Values: []string{},
},
},
},
},
},
},
want: authorizer.AttributesRecord{
User: &user.DefaultInfo{},
ResourceRequest: true,
APIVersion: "*",
LabelSelectorRequirements: labels.Requirements{
mustRequirement("one", "exists", nil),
},
},
enableAuthorizationSelector: true,
},
{
name: "label: requirements DoesNotExist",
args: args{
spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
LabelSelector: &authorizationapi.LabelSelectorAttributes{
Requirements: []metav1.LabelSelectorRequirement{
{
Key: "one",
Operator: "DoesNotExist",
Values: []string{},
},
},
},
},
},
},
want: authorizer.AttributesRecord{
User: &user.DefaultInfo{},
ResourceRequest: true,
APIVersion: "*",
LabelSelectorRequirements: labels.Requirements{
mustRequirement("one", "!", nil),
},
},
enableAuthorizationSelector: true,
},
{
name: "label: requirements bad operator",
args: args{
spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
LabelSelector: &authorizationapi.LabelSelectorAttributes{
Requirements: []metav1.LabelSelectorRequirement{
{
Key: "one",
Operator: "bad",
Values: []string{},
},
},
},
},
},
},
want: authorizer.AttributesRecord{
User: &user.DefaultInfo{},
ResourceRequest: true,
APIVersion: "*",
LabelSelectorParsingErr: utilerrors.NewAggregate([]error{errors.New("\"bad\" is not a valid label selector operator")}),
},
enableAuthorizationSelector: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.enableAuthorizationSelector {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
}
if got := AuthorizationAttributesFrom(tt.args.spec); !reflect.DeepEqual(got, tt.want) {
t.Errorf("AuthorizationAttributesFrom() = %v, want %v", got, tt.want)
if got.LabelSelectorParsingErr != nil {
t.Logf("labelSelectorErr=%q", got.LabelSelectorParsingErr)
}
t.Errorf("AuthorizationAttributesFrom(), got:\n%#v\nwant:\n%#v", got, tt.want)
}
})
}

View File

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

View File

@ -32,6 +32,10 @@ import (
type LabelSelectorValidationOptions struct {
// Allow invalid label value in selector
AllowInvalidLabelValueInSelector bool
// Allows an operator that is not interpretable to pass validation. This is useful for cases where a broader check
// can be performed, as in a *SubjectAccessReview
AllowUnknownOperatorInRequirement bool
}
// LabelSelectorHasInvalidLabelValue returns true if the given selector contains an invalid label value in a match expression.
@ -79,7 +83,9 @@ func ValidateLabelSelectorRequirement(sr metav1.LabelSelectorRequirement, opts L
allErrs = append(allErrs, field.Forbidden(fldPath.Child("values"), "may not be specified when `operator` is 'Exists' or 'DoesNotExist'"))
}
default:
allErrs = append(allErrs, field.Invalid(fldPath.Child("operator"), sr.Operator, "not a valid selector operator"))
if !opts.AllowUnknownOperatorInRequirement {
allErrs = append(allErrs, field.Invalid(fldPath.Child("operator"), sr.Operator, "not a valid selector operator"))
}
}
allErrs = append(allErrs, ValidateLabelName(sr.Key, fldPath.Child("key"))...)
if !opts.AllowInvalidLabelValueInSelector {
@ -113,6 +119,39 @@ func ValidateLabels(labels map[string]string, fldPath *field.Path) field.ErrorLi
return allErrs
}
// FieldSelectorValidationOptions is a struct that can be passed to ValidateFieldSelectorRequirement to record the validate options
type FieldSelectorValidationOptions struct {
// Allows an operator that is not interpretable to pass validation. This is useful for cases where a broader check
// can be performed, as in a *SubjectAccessReview
AllowUnknownOperatorInRequirement bool
}
// ValidateLabelSelectorRequirement validates the requirement according to the opts and returns any validation errors.
func ValidateFieldSelectorRequirement(requirement metav1.FieldSelectorRequirement, opts FieldSelectorValidationOptions, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if len(requirement.Key) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("key"), "must be specified"))
}
switch requirement.Operator {
case metav1.FieldSelectorOpIn, metav1.FieldSelectorOpNotIn:
if len(requirement.Values) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("values"), "must be specified when `operator` is 'In' or 'NotIn'"))
}
case metav1.FieldSelectorOpExists, metav1.FieldSelectorOpDoesNotExist:
if len(requirement.Values) > 0 {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("values"), "may not be specified when `operator` is 'Exists' or 'DoesNotExist'"))
}
default:
if !opts.AllowUnknownOperatorInRequirement {
allErrs = append(allErrs, field.Invalid(fldPath.Child("operator"), requirement.Operator, "not a valid selector operator"))
}
}
return allErrs
}
func ValidateDeleteOptions(options *metav1.DeleteOptions) field.ErrorList {
allErrs := field.ErrorList{}
//lint:file-ignore SA1019 Keep validation for deprecated OrphanDependents option until it's being removed

View File

@ -470,7 +470,7 @@ func TestLabelSelectorMatchExpression(t *testing.T) {
}}
for index, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
allErrs := ValidateLabelSelector(testCase.labelSelector, LabelSelectorValidationOptions{false}, field.NewPath("labelSelector"))
allErrs := ValidateLabelSelector(testCase.labelSelector, LabelSelectorValidationOptions{AllowInvalidLabelValueInSelector: false}, field.NewPath("labelSelector"))
if len(allErrs) != testCase.wantErrorNumber {
t.Errorf("case[%d]: expected failure", index)
}

View File

@ -45,6 +45,19 @@ var (
// Requirements is AND of all requirements.
type Requirements []Requirement
func (r Requirements) String() string {
var sb strings.Builder
for i, requirement := range r {
if i > 0 {
sb.WriteString(", ")
}
sb.WriteString(requirement.String())
}
return sb.String()
}
// Selector represents a label selector.
type Selector interface {
// Matches returns true if this selector matches the given set of labels.
@ -285,6 +298,13 @@ func (r *Requirement) Values() sets.String {
return ret
}
// ValuesUnsorted returns a copy of requirement values as passed to NewRequirement without sorting.
func (r *Requirement) ValuesUnsorted() []string {
ret := make([]string, 0, len(r.strValues))
ret = append(ret, r.strValues...)
return ret
}
// Equal checks the equality of requirement.
func (r Requirement) Equal(x Requirement) bool {
if r.key != x.key {

View File

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,12 @@ package filters
import (
"context"
"errors"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"net/http"
"net/http/httptest"
"reflect"
@ -34,10 +40,16 @@ import (
)
func TestGetAuthorizerAttributes(t *testing.T) {
basicLabelRequirement, err := labels.NewRequirement("foo", selection.DoubleEquals, []string{"bar"})
if err != nil {
t.Fatal(err)
}
testcases := map[string]struct {
Verb string
Path string
ExpectedAttributes *authorizer.AttributesRecord
Verb string
Path string
ExpectedAttributes *authorizer.AttributesRecord
EnableAuthorizationSelector bool
}{
"non-resource root": {
Verb: "POST",
@ -102,26 +114,122 @@ func TestGetAuthorizerAttributes(t *testing.T) {
Resource: "jobs",
},
},
"disabled, ignore good field selector": {
Verb: "GET",
Path: "/apis/batch/v1/namespaces/myns/jobs?fieldSelector%=foo%3Dbar",
ExpectedAttributes: &authorizer.AttributesRecord{
Verb: "list",
Path: "/apis/batch/v1/namespaces/myns/jobs",
ResourceRequest: true,
APIGroup: batch.GroupName,
APIVersion: "v1",
Namespace: "myns",
Resource: "jobs",
},
},
"enabled, good field selector": {
Verb: "GET",
Path: "/apis/batch/v1/namespaces/myns/jobs?fieldSelector=foo%3D%3Dbar",
ExpectedAttributes: &authorizer.AttributesRecord{
Verb: "list",
Path: "/apis/batch/v1/namespaces/myns/jobs",
ResourceRequest: true,
APIGroup: batch.GroupName,
APIVersion: "v1",
Namespace: "myns",
Resource: "jobs",
FieldSelectorRequirements: fields.Requirements{
fields.OneTermEqualSelector("foo", "bar").Requirements()[0],
},
},
EnableAuthorizationSelector: true,
},
"enabled, bad field selector": {
Verb: "GET",
Path: "/apis/batch/v1/namespaces/myns/jobs?fieldSelector=%2Abar",
ExpectedAttributes: &authorizer.AttributesRecord{
Verb: "list",
Path: "/apis/batch/v1/namespaces/myns/jobs",
ResourceRequest: true,
APIGroup: batch.GroupName,
APIVersion: "v1",
Namespace: "myns",
Resource: "jobs",
FieldSelectorParsingErr: errors.New("invalid selector: '*bar'; can't understand '*bar'"),
},
EnableAuthorizationSelector: true,
},
"disabled, ignore good label selector": {
Verb: "GET",
Path: "/apis/batch/v1/namespaces/myns/jobs?labelSelector%=foo%3Dbar",
ExpectedAttributes: &authorizer.AttributesRecord{
Verb: "list",
Path: "/apis/batch/v1/namespaces/myns/jobs",
ResourceRequest: true,
APIGroup: batch.GroupName,
APIVersion: "v1",
Namespace: "myns",
Resource: "jobs",
},
},
"enabled, good label selector": {
Verb: "GET",
Path: "/apis/batch/v1/namespaces/myns/jobs?labelSelector=foo%3D%3Dbar",
ExpectedAttributes: &authorizer.AttributesRecord{
Verb: "list",
Path: "/apis/batch/v1/namespaces/myns/jobs",
ResourceRequest: true,
APIGroup: batch.GroupName,
APIVersion: "v1",
Namespace: "myns",
Resource: "jobs",
LabelSelectorRequirements: labels.Requirements{
*basicLabelRequirement,
},
},
EnableAuthorizationSelector: true,
},
"enabled, bad label selector": {
Verb: "GET",
Path: "/apis/batch/v1/namespaces/myns/jobs?labelSelector=%2Abar",
ExpectedAttributes: &authorizer.AttributesRecord{
Verb: "list",
Path: "/apis/batch/v1/namespaces/myns/jobs",
ResourceRequest: true,
APIGroup: batch.GroupName,
APIVersion: "v1",
Namespace: "myns",
Resource: "jobs",
LabelSelectorParsingErr: errors.New("unable to parse requirement: <nil>: Invalid value: \"*bar\": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')"),
},
EnableAuthorizationSelector: true,
},
}
for k, tc := range testcases {
req, _ := http.NewRequest(tc.Verb, tc.Path, nil)
req.RemoteAddr = "127.0.0.1"
t.Run(k, func(t *testing.T) {
if tc.EnableAuthorizationSelector {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
}
var attribs authorizer.Attributes
var err error
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
attribs, err = GetAuthorizerAttributes(ctx)
req, _ := http.NewRequest(tc.Verb, tc.Path, nil)
req.RemoteAddr = "127.0.0.1"
var attribs authorizer.Attributes
var err error
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
attribs, err = GetAuthorizerAttributes(ctx)
})
handler = WithRequestInfo(handler, newTestRequestInfoResolver())
handler.ServeHTTP(httptest.NewRecorder(), req)
if err != nil {
t.Errorf("%s: unexpected error: %v", k, err)
} else if !reflect.DeepEqual(attribs, tc.ExpectedAttributes) {
t.Errorf("%s: expected\n\t%#v\ngot\n\t%#v", k, tc.ExpectedAttributes, attribs)
}
})
handler = WithRequestInfo(handler, newTestRequestInfoResolver())
handler.ServeHTTP(httptest.NewRecorder(), req)
if err != nil {
t.Errorf("%s: unexpected error: %v", k, err)
} else if !reflect.DeepEqual(attribs, tc.ExpectedAttributes) {
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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/klog/v2"
)
@ -62,6 +64,13 @@ type RequestInfo struct {
Name string
// Parts are the path parts for the request, always starting with /{resource}/{name}
Parts []string
// FieldSelector contains the unparsed field selector from a request. It is only present if the apiserver
// honors field selectors for the verb this request is associated with.
FieldSelector string
// LabelSelector contains the unparsed field selector from a request. It is only present if the apiserver
// honors field selectors for the verb this request is associated with.
LabelSelector string
}
// specialVerbs contains just strings which are used in REST paths for special actions that don't fall under the normal
@ -77,6 +86,9 @@ var specialVerbsNoSubresources = sets.NewString("proxy")
// this list allows the parser to distinguish between a namespace subresource, and a namespaced resource
var namespaceSubresources = sets.NewString("status", "finalize")
// verbsWithSelectors is the list of verbs which support fieldSelector and labelSelector parameters
var verbsWithSelectors = sets.NewString("list", "watch", "deletecollection")
// NamespaceSubResourcesForTest exports namespaceSubresources for testing in pkg/controlplane/master_test.go, so we never drift
var NamespaceSubResourcesForTest = sets.NewString(namespaceSubresources.List()...)
@ -151,6 +163,7 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er
currentParts = currentParts[1:]
// handle input of form /{specialVerb}/*
verbViaPathPrefix := false
if specialVerbs.Has(currentParts[0]) {
if len(currentParts) < 2 {
return &requestInfo, fmt.Errorf("unable to determine kind and namespace from url, %v", req.URL)
@ -158,6 +171,7 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er
requestInfo.Verb = currentParts[0]
currentParts = currentParts[1:]
verbViaPathPrefix = true
} else {
switch req.Method {
@ -238,11 +252,28 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er
}
}
}
// if there's no name on the request and we thought it was a delete before, then the actual verb is deletecollection
if len(requestInfo.Name) == 0 && requestInfo.Verb == "delete" {
requestInfo.Verb = "deletecollection"
}
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) {
// Don't support selector authorization on requests that used the deprecated verb-via-path mechanism, since they don't support selectors consistently.
// There are multi-object and single-object watch endpoints, and only the multi-object one supports selectors.
if !verbViaPathPrefix && verbsWithSelectors.Has(requestInfo.Verb) {
// interestingly these are parsed above, but the current structure there means that if one (or anything) in the
// listOptions fails to decode, the field and label selectors are lost.
// therefore, do the straight query param read here.
if vals := req.URL.Query()["fieldSelector"]; len(vals) > 0 {
requestInfo.FieldSelector = vals[0]
}
if vals := req.URL.Query()["labelSelector"]; len(vals) > 0 {
requestInfo.LabelSelector = vals[0]
}
}
}
return &requestInfo, nil
}

View File

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

View File

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

View File

@ -81,13 +81,15 @@ func v1beta1ResourceAttributesToV1ResourceAttributes(in *authorizationv1beta1.Re
return nil
}
return &authorizationv1.ResourceAttributes{
Namespace: in.Namespace,
Verb: in.Verb,
Group: in.Group,
Version: in.Version,
Resource: in.Resource,
Subresource: in.Subresource,
Name: in.Name,
Namespace: in.Namespace,
Verb: in.Verb,
Group: in.Group,
Version: in.Version,
Resource: in.Resource,
Subresource: in.Subresource,
Name: in.Name,
FieldSelector: in.FieldSelector,
LabelSelector: in.LabelSelector,
}
}

View File

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