Include empty string attributes for CEL authz evaluation

This commit is contained in:
Jordan Liggitt
2023-11-02 15:14:06 -04:00
parent 2e2f51a441
commit 44d89c8cf8
3 changed files with 54 additions and 22 deletions

View File

@@ -22,6 +22,7 @@ import (
"github.com/google/cel-go/cel" "github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/common/types/ref"
authorizationv1 "k8s.io/api/authorization/v1"
"k8s.io/apimachinery/pkg/util/version" "k8s.io/apimachinery/pkg/util/version"
apiservercel "k8s.io/apiserver/pkg/cel" apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/environment" "k8s.io/apiserver/pkg/cel/environment"
@@ -143,6 +144,7 @@ func mustBuildEnv(baseEnv *environment.EnvSet) *environment.EnvSet {
} }
// buildRequestType generates a DeclType for SubjectAccessReviewSpec. // buildRequestType generates a DeclType for SubjectAccessReviewSpec.
// if attributes are added here, also add to convertObjectToUnstructured.
func buildRequestType(field func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField, fields func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField) *apiservercel.DeclType { func buildRequestType(field func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField, fields func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField) *apiservercel.DeclType {
resourceAttributesType := buildResourceAttributesType(field, fields) resourceAttributesType := buildResourceAttributesType(field, fields)
nonResourceAttributesType := buildNonResourceAttributesType(field, fields) nonResourceAttributesType := buildNonResourceAttributesType(field, fields)
@@ -157,6 +159,7 @@ func buildRequestType(field func(name string, declType *apiservercel.DeclType, r
} }
// buildResourceAttributesType generates a DeclType for ResourceAttributes. // buildResourceAttributesType generates a DeclType for ResourceAttributes.
// if attributes are added here, also add to convertObjectToUnstructured.
func buildResourceAttributesType(field func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField, fields func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField) *apiservercel.DeclType { func buildResourceAttributesType(field func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField, fields func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField) *apiservercel.DeclType {
return apiservercel.NewObjectType("kubernetes.ResourceAttributes", fields( return apiservercel.NewObjectType("kubernetes.ResourceAttributes", fields(
field("namespace", apiservercel.StringType, false), field("namespace", apiservercel.StringType, false),
@@ -170,9 +173,42 @@ func buildResourceAttributesType(field func(name string, declType *apiservercel.
} }
// buildNonResourceAttributesType generates a DeclType for NonResourceAttributes. // buildNonResourceAttributesType generates a DeclType for NonResourceAttributes.
// if attributes are added here, also add to convertObjectToUnstructured.
func buildNonResourceAttributesType(field func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField, fields func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField) *apiservercel.DeclType { func buildNonResourceAttributesType(field func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField, fields func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField) *apiservercel.DeclType {
return apiservercel.NewObjectType("kubernetes.NonResourceAttributes", fields( return apiservercel.NewObjectType("kubernetes.NonResourceAttributes", fields(
field("path", apiservercel.StringType, false), field("path", apiservercel.StringType, false),
field("verb", apiservercel.StringType, false), field("verb", apiservercel.StringType, false),
)) ))
} }
func convertObjectToUnstructured(obj *authorizationv1.SubjectAccessReviewSpec) map[string]interface{} {
// Construct version containing every SubjectAccessReview user and string attribute field, even omitempty ones, for evaluation by CEL
extra := obj.Extra
if extra == nil {
extra = map[string]authorizationv1.ExtraValue{}
}
ret := map[string]interface{}{
"user": obj.User,
"groups": obj.Groups,
"uid": string(obj.UID),
"extra": extra,
}
if obj.ResourceAttributes != nil {
ret["resourceAttributes"] = map[string]string{
"namespace": obj.ResourceAttributes.Namespace,
"verb": obj.ResourceAttributes.Verb,
"group": obj.ResourceAttributes.Group,
"version": obj.ResourceAttributes.Version,
"resource": obj.ResourceAttributes.Resource,
"subresource": obj.ResourceAttributes.Subresource,
"name": obj.ResourceAttributes.Name,
}
}
if obj.NonResourceAttributes != nil {
ret["nonResourceAttributes"] = map[string]string{
"verb": obj.NonResourceAttributes.Verb,
"path": obj.NonResourceAttributes.Path,
}
}
return ret
}

View File

@@ -21,8 +21,8 @@ import (
"fmt" "fmt"
celgo "github.com/google/cel-go/cel" celgo "github.com/google/cel-go/cel"
authorizationv1 "k8s.io/api/authorization/v1" authorizationv1 "k8s.io/api/authorization/v1"
"k8s.io/apimachinery/pkg/runtime"
utilerrors "k8s.io/apimachinery/pkg/util/errors" utilerrors "k8s.io/apimachinery/pkg/util/errors"
) )
@@ -33,12 +33,8 @@ type CELMatcher struct {
// eval evaluates the given SubjectAccessReview against all cel matchCondition expression // eval evaluates the given SubjectAccessReview against all cel matchCondition expression
func (c *CELMatcher) Eval(ctx context.Context, r *authorizationv1.SubjectAccessReview) (bool, error) { func (c *CELMatcher) Eval(ctx context.Context, r *authorizationv1.SubjectAccessReview) (bool, error) {
var evalErrors []error var evalErrors []error
specValObject, err := convertObjectToUnstructured(&r.Spec)
if err != nil {
return false, fmt.Errorf("authz celMatcher eval error: convert SubjectAccessReviewSpec object to unstructured failed: %w", err)
}
va := map[string]interface{}{ va := map[string]interface{}{
"request": specValObject, "request": convertObjectToUnstructured(&r.Spec),
} }
for _, compilationResult := range c.CompilationResults { for _, compilationResult := range c.CompilationResults {
evalResult, _, err := compilationResult.Program.ContextEval(ctx, va) evalResult, _, err := compilationResult.Program.ContextEval(ctx, va)
@@ -68,14 +64,3 @@ func (c *CELMatcher) Eval(ctx context.Context, r *authorizationv1.SubjectAccessR
// return ALL matchConditions evaluate to TRUE successfully without error // return ALL matchConditions evaluate to TRUE successfully without error
return true, nil return true, nil
} }
func convertObjectToUnstructured(obj *authorizationv1.SubjectAccessReviewSpec) (map[string]interface{}, error) {
if obj == nil {
return nil, nil
}
ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return nil, err
}
return ret, nil
}

View File

@@ -704,6 +704,7 @@ func TestStructuredAuthzConfigFeatureEnablement(t *testing.T) {
Name: "alice", Name: "alice",
UID: "1", UID: "1",
Groups: []string{"group1", "group2"}, Groups: []string{"group1", "group2"},
Extra: map[string][]string{"key1": {"a", "b", "c"}},
}, },
ResourceRequest: true, ResourceRequest: true,
Namespace: "kittensandponies", Namespace: "kittensandponies",
@@ -798,6 +799,7 @@ func TestV1WebhookMatchConditions(t *testing.T) {
Name: "alice", Name: "alice",
UID: "1", UID: "1",
Groups: []string{"group1", "group2"}, Groups: []string{"group1", "group2"},
Extra: map[string][]string{"key1": {"a", "b", "c"}},
}, },
ResourceRequest: true, ResourceRequest: true,
Namespace: "kittensandponies", Namespace: "kittensandponies",
@@ -848,6 +850,18 @@ func TestV1WebhookMatchConditions(t *testing.T) {
{ {
Expression: "('group1' in request.groups)", Expression: "('group1' in request.groups)",
}, },
{
Expression: "('key1' in request.extra)",
},
{
Expression: "!('key2' in request.extra)",
},
{
Expression: "('a' in request.extra['key1'])",
},
{
Expression: "!('z' in request.extra['key1'])",
},
{ {
Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'", Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'",
}, },
@@ -1028,13 +1042,10 @@ func TestV1WebhookMatchConditions(t *testing.T) {
expectedCompileErr: "", expectedCompileErr: "",
// default decisionOnError in newWithBackoff to skip // default decisionOnError in newWithBackoff to skip
expectedDecision: authorizer.DecisionNoOpinion, expectedDecision: authorizer.DecisionNoOpinion,
expectedEvalErr: "[cel evaluation error: expression 'request.user == 'bob'' resulted in error: no such key: user, cel evaluation error: expression 'has(request.nonResourceAttributes) && request.nonResourceAttributes.verb == 'get'' resulted in error: no such key: verb]", expectedEvalErr: "cel evaluation error: expression 'request.resourceAttributes.verb == 'get'' resulted in error: no such key: resourceAttributes",
expressions: []apiserver.WebhookMatchCondition{ expressions: []apiserver.WebhookMatchCondition{
{ {
Expression: "request.user == 'bob'", Expression: "request.resourceAttributes.verb == 'get'",
},
{
Expression: "has(request.nonResourceAttributes) && request.nonResourceAttributes.verb == 'get'",
}, },
}, },
}, },