k8s.io/apiserver@v0.31.1/pkg/admission/plugin/cel/compile_test.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cel
    18  
    19  import (
    20  	"math/rand"
    21  	"strings"
    22  	"testing"
    23  
    24  	celgo "github.com/google/cel-go/cel"
    25  
    26  	"k8s.io/apimachinery/pkg/util/version"
    27  	"k8s.io/apiserver/pkg/cel/environment"
    28  	"k8s.io/apiserver/pkg/cel/library"
    29  )
    30  
    31  func TestCompileValidatingPolicyExpression(t *testing.T) {
    32  	cases := []struct {
    33  		name             string
    34  		expressions      []string
    35  		hasParams        bool
    36  		hasAuthorizer    bool
    37  		errorExpressions map[string]string
    38  		envType          environment.Type
    39  	}{
    40  		{
    41  			name: "invalid syntax",
    42  			errorExpressions: map[string]string{
    43  				"1 < 'asdf'":          "found no matching overload for '_<_' applied to '(int, string)",
    44  				"'asdf'.contains('x'": "Syntax error: missing ')' at",
    45  			},
    46  		},
    47  		{
    48  			name:        "with params",
    49  			expressions: []string{"object.foo < params.x"},
    50  			hasParams:   true,
    51  		},
    52  		{
    53  			name:        "namespaceObject",
    54  			expressions: []string{"namespaceObject.metadata.name.startsWith('test')"},
    55  			hasParams:   true,
    56  		},
    57  		{
    58  			name:             "without params",
    59  			errorExpressions: map[string]string{"object.foo < params.x": "undeclared reference to 'params'"},
    60  			hasParams:        false,
    61  		},
    62  		{
    63  			name:        "oldObject comparison",
    64  			expressions: []string{"object.foo == oldObject.foo"},
    65  		},
    66  		{
    67  			name: "object null checks",
    68  			// since object and oldObject are CEL variable, has() cannot be used (it works only on fields),
    69  			// so we always populate it to allow for a null check in the case of CREATE, where oldObject is
    70  			// null, and DELETE, where object is null.
    71  			expressions: []string{"object == null || oldObject == null || object.foo == oldObject.foo"},
    72  		},
    73  		{
    74  			name:             "invalid root var",
    75  			errorExpressions: map[string]string{"object.foo < invalid.x": "undeclared reference to 'invalid'"},
    76  			hasParams:        false,
    77  		},
    78  		{
    79  			name: "function library",
    80  			// sanity check that functions of the various libraries are available
    81  			expressions: []string{
    82  				"object.spec.string.matches('[0-9]+')",                      // strings extension lib
    83  				"object.spec.string.findAll('[0-9]+').size() > 0",           // kubernetes string lib
    84  				"object.spec.list.isSorted()",                               // kubernetes list lib
    85  				"url(object.spec.endpoint).getHostname() in ['ok1', 'ok2']", // kubernetes url lib
    86  			},
    87  		},
    88  		{
    89  			name: "valid request",
    90  			expressions: []string{
    91  				"request.kind.group == 'example.com' && request.kind.version == 'v1' && request.kind.kind == 'Fake'",
    92  				"request.resource.group == 'example.com' && request.resource.version == 'v1' && request.resource.resource == 'fake' && request.subResource == 'scale'",
    93  				"request.requestKind.group == 'example.com' && request.requestKind.version == 'v1' && request.requestKind.kind == 'Fake'",
    94  				"request.requestResource.group == 'example.com' && request.requestResource.version == 'v1' && request.requestResource.resource == 'fake' && request.requestSubResource == 'scale'",
    95  				"request.name == 'fake-name'",
    96  				"request.namespace == 'fake-namespace'",
    97  				"request.operation == 'CREATE'",
    98  				"request.userInfo.username == 'admin'",
    99  				"request.userInfo.uid == '014fbff9a07c'",
   100  				"request.userInfo.groups == ['system:authenticated', 'my-admin-group']",
   101  				"request.userInfo.extra == {'some-key': ['some-value1', 'some-value2']}",
   102  				"request.dryRun == false",
   103  				"request.options == {'whatever': 'you want'}",
   104  			},
   105  		},
   106  		{
   107  			name: "invalid request",
   108  			errorExpressions: map[string]string{
   109  				"request.foo1 == 'nope'":                 "undefined field 'foo1'",
   110  				"request.resource.foo2 == 'nope'":        "undefined field 'foo2'",
   111  				"request.requestKind.foo3 == 'nope'":     "undefined field 'foo3'",
   112  				"request.requestResource.foo4 == 'nope'": "undefined field 'foo4'",
   113  				"request.userInfo.foo5 == 'nope'":        "undefined field 'foo5'",
   114  			},
   115  		},
   116  		{
   117  			name:          "with authorizer",
   118  			hasAuthorizer: true,
   119  			expressions: []string{
   120  				"authorizer.group('') != null",
   121  			},
   122  		},
   123  		{
   124  			name: "without authorizer",
   125  			errorExpressions: map[string]string{
   126  				"authorizer.group('') != null": "undeclared reference to 'authorizer'",
   127  			},
   128  		},
   129  		{
   130  			name: "compile with storage environment should recognize functions available only in the storage environment",
   131  			expressions: []string{
   132  				"test() == true",
   133  			},
   134  			envType: environment.StoredExpressions,
   135  		},
   136  		{
   137  			name: "compile with supported environment should not recognize functions available only in the storage environment",
   138  			errorExpressions: map[string]string{
   139  				"test() == true": "undeclared reference to 'test'",
   140  			},
   141  			envType: environment.NewExpressions,
   142  		},
   143  		{
   144  			name: "valid namespaceObject",
   145  			expressions: []string{
   146  				"namespaceObject.metadata != null",
   147  				"namespaceObject.metadata.name == 'test'",
   148  				"namespaceObject.metadata.generateName == 'test'",
   149  				"namespaceObject.metadata.namespace == 'testns'",
   150  				"'test' in namespaceObject.metadata.labels",
   151  				"'test' in namespaceObject.metadata.annotations",
   152  				"namespaceObject.metadata.UID == '12345'",
   153  				"type(namespaceObject.metadata.creationTimestamp) == google.protobuf.Timestamp",
   154  				"type(namespaceObject.metadata.deletionTimestamp) == google.protobuf.Timestamp",
   155  				"namespaceObject.metadata.deletionGracePeriodSeconds == 5",
   156  				"namespaceObject.metadata.generation == 2",
   157  				"namespaceObject.metadata.resourceVersion == 'v1'",
   158  				"namespaceObject.metadata.finalizers[0] == 'testEnv'",
   159  				"namespaceObject.spec.finalizers[0] == 'testEnv'",
   160  				"namespaceObject.status.phase == 'Active'",
   161  				"namespaceObject.status.conditions[0].status == 'True'",
   162  				"namespaceObject.status.conditions[0].type == 'NamespaceDeletionDiscoveryFailure'",
   163  				"type(namespaceObject.status.conditions[0].lastTransitionTime) == google.protobuf.Timestamp",
   164  				"namespaceObject.status.conditions[0].message == 'Unknow'",
   165  				"namespaceObject.status.conditions[0].reason == 'Invalid'",
   166  			},
   167  		},
   168  		{
   169  			name: "invalid namespaceObject",
   170  			errorExpressions: map[string]string{
   171  				"namespaceObject.foo1 == 'nope'":                      "undefined field 'foo1'",
   172  				"namespaceObject.metadata.foo2 == 'nope'":             "undefined field 'foo2'",
   173  				"namespaceObject.spec.foo3 == 'nope'":                 "undefined field 'foo3'",
   174  				"namespaceObject.status.foo4 == 'nope'":               "undefined field 'foo4'",
   175  				"namespaceObject.status.conditions[0].foo5 == 'nope'": "undefined field 'foo5'",
   176  			},
   177  		},
   178  	}
   179  
   180  	// Include the test library, which includes the test() function in the storage environment during test
   181  	base := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)
   182  	extended, err := base.Extend(environment.VersionedOptions{
   183  		IntroducedVersion: version.MajorMinor(1, 999),
   184  		EnvOptions:        []celgo.EnvOption{library.Test()},
   185  	})
   186  	if err != nil {
   187  		t.Fatal(err)
   188  	}
   189  	compiler := NewCompiler(extended)
   190  
   191  	for _, tc := range cases {
   192  		envType := tc.envType
   193  		if envType == "" {
   194  			envType = environment.NewExpressions
   195  		}
   196  		t.Run(tc.name, func(t *testing.T) {
   197  			for _, expr := range tc.expressions {
   198  				t.Run(expr, func(t *testing.T) {
   199  					t.Run("expression", func(t *testing.T) {
   200  						options := OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}
   201  
   202  						result := compiler.CompileCELExpression(&fakeValidationCondition{
   203  							Expression: expr,
   204  						}, options, envType)
   205  						if result.Error != nil {
   206  							t.Errorf("Unexpected error: %v", result.Error)
   207  						}
   208  					})
   209  					t.Run("auditAnnotation.valueExpression", func(t *testing.T) {
   210  						// Test audit annotation compilation by casting the result to a string
   211  						options := OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}
   212  						result := compiler.CompileCELExpression(&fakeAuditAnnotationCondition{
   213  							ValueExpression: "string(" + expr + ")",
   214  						}, options, envType)
   215  						if result.Error != nil {
   216  							t.Errorf("Unexpected error: %v", result.Error)
   217  						}
   218  					})
   219  				})
   220  			}
   221  			for expr, expectErr := range tc.errorExpressions {
   222  				t.Run(expr, func(t *testing.T) {
   223  					t.Run("expression", func(t *testing.T) {
   224  						options := OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}
   225  						result := compiler.CompileCELExpression(&fakeValidationCondition{
   226  							Expression: expr,
   227  						}, options, envType)
   228  						if result.Error == nil {
   229  							t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr)
   230  							return
   231  						}
   232  						if !strings.Contains(result.Error.Error(), expectErr) {
   233  							t.Errorf("Expected validation '%s' error to contain '%v' but got: %v", expr, expectErr, result.Error)
   234  						}
   235  					})
   236  					t.Run("auditAnnotation.valueExpression", func(t *testing.T) {
   237  						// Test audit annotation compilation by casting the result to a string
   238  						options := OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}
   239  						result := compiler.CompileCELExpression(&fakeAuditAnnotationCondition{
   240  							ValueExpression: "string(" + expr + ")",
   241  						}, options, envType)
   242  						if result.Error == nil {
   243  							t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr)
   244  							return
   245  						}
   246  						if !strings.Contains(result.Error.Error(), expectErr) {
   247  							t.Errorf("Expected validation '%s' error to contain '%v' but got: %v", expr, expectErr, result.Error)
   248  						}
   249  					})
   250  				})
   251  			}
   252  		})
   253  	}
   254  }
   255  
   256  func BenchmarkCompile(b *testing.B) {
   257  	compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
   258  	b.ResetTimer()
   259  	for i := 0; i < b.N; i++ {
   260  		options := OptionalVariableDeclarations{HasParams: rand.Int()%2 == 0, HasAuthorizer: rand.Int()%2 == 0}
   261  
   262  		result := compiler.CompileCELExpression(&fakeValidationCondition{
   263  			Expression: "object.foo < object.bar",
   264  		}, options, environment.StoredExpressions)
   265  		if result.Error != nil {
   266  			b.Fatal(result.Error)
   267  		}
   268  	}
   269  }
   270  
   271  type fakeValidationCondition struct {
   272  	Expression string
   273  }
   274  
   275  func (v *fakeValidationCondition) GetExpression() string {
   276  	return v.Expression
   277  }
   278  
   279  func (v *fakeValidationCondition) ReturnTypes() []*celgo.Type {
   280  	return []*celgo.Type{celgo.BoolType}
   281  }
   282  
   283  type fakeAuditAnnotationCondition struct {
   284  	ValueExpression string
   285  }
   286  
   287  func (v *fakeAuditAnnotationCondition) GetExpression() string {
   288  	return v.ValueExpression
   289  }
   290  
   291  func (v *fakeAuditAnnotationCondition) ReturnTypes() []*celgo.Type {
   292  	return []*celgo.Type{celgo.StringType, celgo.NullType}
   293  }