Include empty string attributes for CEL authz evaluation
This commit is contained in:
		@@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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'",
 | 
					 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user