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 }