Skip to content

Commit

Permalink
Add CEL fieldSelector / labelSelector support to authorizer library
Browse files Browse the repository at this point in the history
  • Loading branch information
deads2k authored and liggitt committed Jul 19, 2024
1 parent 03d48b7 commit be2e32f
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ import (
"context"
"errors"
"fmt"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"reflect"
"strings"
"testing"
Expand Down Expand Up @@ -125,6 +131,11 @@ func TestCompile(t *testing.T) {
}

func TestFilter(t *testing.T) {
simpleLabelSelector, err := labels.NewRequirement("apple", selection.Equals, []string{"banana"})
if err != nil {
panic(err)
}

configMapParams := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Expand Down Expand Up @@ -183,6 +194,7 @@ func TestFilter(t *testing.T) {
testPerCallLimit uint64
namespaceObject *corev1.Namespace
strictCost bool
enableSelectors bool
}{
{
name: "valid syntax for object",
Expand Down Expand Up @@ -486,7 +498,65 @@ func TestFilter(t *testing.T) {
name: "test authorizer allow resource check with all fields",
validations: []ExpressionAccessor{
&condition{
Expression: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()",
Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo=bar').labelSelector('apple=banana').subresource('status').namespace('test').name('backend').check('create').allowed()",
},
},
attributes: newValidAttribute(&podObject, false),
results: []EvaluationResult{
{
EvalResult: celtypes.True,
},
},
authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{
ResourceRequest: true,
APIGroup: "apps",
Resource: "deployments",
Subresource: "status",
Namespace: "test",
Name: "backend",
Verb: "create",
APIVersion: "*",
FieldSelectorRequirements: fields.Requirements{
{Operator: "=", Field: "foo", Value: "bar"},
},
LabelSelectorRequirements: labels.Requirements{
*simpleLabelSelector,
},
}),
enableSelectors: true,
},
{
name: "test authorizer allow resource check with parse failures",
validations: []ExpressionAccessor{
&condition{
Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo badoperator bar').labelSelector('apple badoperator banana').subresource('status').namespace('test').name('backend').check('create').allowed()",
},
},
attributes: newValidAttribute(&podObject, false),
results: []EvaluationResult{
{
EvalResult: celtypes.True,
},
},
authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{
ResourceRequest: true,
APIGroup: "apps",
Resource: "deployments",
Subresource: "status",
Namespace: "test",
Name: "backend",
Verb: "create",
APIVersion: "*",
FieldSelectorParsingErr: errors.New("invalid selector: 'foo badoperator bar'; can't understand 'foo badoperator bar'"),
LabelSelectorParsingErr: errors.New("unable to parse requirement: found 'badoperator', expected: in, notin, =, ==, !=, gt, lt"),
}),
enableSelectors: true,
},
{
name: "test authorizer allow resource check with all fields, without gate",
validations: []ExpressionAccessor{
&condition{
Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo=bar').labelSelector('apple=banana').subresource('status').namespace('test').name('backend').check('create').allowed()",
},
},
attributes: newValidAttribute(&podObject, false),
Expand Down Expand Up @@ -760,6 +830,10 @@ func TestFilter(t *testing.T) {

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if tc.enableSelectors {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
}

if tc.testPerCallLimit == 0 {
tc.testPerCallLimit = celconfig.PerCallLimit
}
Expand Down Expand Up @@ -1400,6 +1474,7 @@ func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes)
if !ok {
panic(fmt.Sprintf("unsupported type: %T", a))
}

if reflect.DeepEqual(f.match.match, *other) {
return f.match.decision, f.match.reason, f.match.err
}
Expand Down
74 changes: 69 additions & 5 deletions staging/src/k8s.io/apiserver/pkg/cel/library/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ package library
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"reflect"
"strings"

Expand Down Expand Up @@ -222,6 +226,12 @@ var authzLibraryDecls = map[string][]cel.FunctionOpt{
"subresource": {
cel.MemberOverload("resourcecheck_subresource", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
cel.BinaryBinding(resourceCheckSubresource))},
"fieldSelector": {
cel.MemberOverload("authorizer_fieldselector", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
cel.BinaryBinding(resourceCheckFieldSelector))},
"labelSelector": {
cel.MemberOverload("authorizer_labelselector", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
cel.BinaryBinding(resourceCheckLabelSelector))},
"namespace": {
cel.MemberOverload("resourcecheck_namespace", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
cel.BinaryBinding(resourceCheckNamespace))},
Expand Down Expand Up @@ -354,6 +364,38 @@ func resourceCheckSubresource(arg1, arg2 ref.Val) ref.Val {
return result
}

func resourceCheckFieldSelector(arg1, arg2 ref.Val) ref.Val {
resourceCheck, ok := arg1.(resourceCheckVal)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}

fieldSelector, ok := arg2.Value().(string)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}

result := resourceCheck
result.fieldSelector = fieldSelector
return result
}

func resourceCheckLabelSelector(arg1, arg2 ref.Val) ref.Val {
resourceCheck, ok := arg1.(resourceCheckVal)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}

labelSelector, ok := arg2.Value().(string)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}

result := resourceCheck
result.labelSelector = labelSelector
return result
}

func resourceCheckNamespace(arg1, arg2 ref.Val) ref.Val {
resourceCheck, ok := arg1.(resourceCheckVal)
if !ok {
Expand Down Expand Up @@ -544,11 +586,13 @@ func (g groupCheckVal) resourceCheck(resource string) resourceCheckVal {

type resourceCheckVal struct {
receiverOnlyObjectVal
groupCheck groupCheckVal
resource string
subresource string
namespace string
name string
groupCheck groupCheckVal
resource string
subresource string
namespace string
name string
fieldSelector string
labelSelector string
}

func (a resourceCheckVal) Authorize(ctx context.Context, verb string) ref.Val {
Expand All @@ -563,6 +607,26 @@ func (a resourceCheckVal) Authorize(ctx context.Context, verb string) ref.Val {
Verb: verb,
User: a.groupCheck.authorizer.userInfo,
}

if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) {
if len(a.fieldSelector) > 0 {
selector, err := fields.ParseSelector(a.fieldSelector)
if err != nil {
attr.FieldSelectorRequirements, attr.FieldSelectorParsingErr = nil, err
} else {
attr.FieldSelectorRequirements, attr.FieldSelectorParsingErr = selector.Requirements(), nil
}
}
if len(a.labelSelector) > 0 {
requirements, err := labels.ParseToRequirements(a.labelSelector)
if err != nil {
attr.LabelSelectorRequirements, attr.LabelSelectorParsingErr = nil, err
} else {
attr.LabelSelectorRequirements, attr.LabelSelectorParsingErr = requirements, nil
}
}
}

decision, reason, err := a.groupCheck.authorizer.authAuthorizer.Authorize(ctx, attr)
return newDecision(decision, err, reason)
}
Expand Down

0 comments on commit be2e32f

Please sign in to comment.