k8s.io/apiserver@v0.31.1/pkg/apis/apiserver/validation/validation.go (about) 1 /* 2 Copyright 2023 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 validation 18 19 import ( 20 "errors" 21 "fmt" 22 "net/url" 23 "os" 24 "path/filepath" 25 "strings" 26 "time" 27 28 celgo "github.com/google/cel-go/cel" 29 "github.com/google/cel-go/common/operators" 30 exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" 31 32 v1 "k8s.io/api/authorization/v1" 33 "k8s.io/api/authorization/v1beta1" 34 "k8s.io/apimachinery/pkg/util/sets" 35 utilvalidation "k8s.io/apimachinery/pkg/util/validation" 36 "k8s.io/apimachinery/pkg/util/validation/field" 37 api "k8s.io/apiserver/pkg/apis/apiserver" 38 authenticationcel "k8s.io/apiserver/pkg/authentication/cel" 39 authorizationcel "k8s.io/apiserver/pkg/authorization/cel" 40 "k8s.io/apiserver/pkg/cel" 41 "k8s.io/apiserver/pkg/cel/environment" 42 "k8s.io/apiserver/pkg/features" 43 utilfeature "k8s.io/apiserver/pkg/util/feature" 44 "k8s.io/client-go/util/cert" 45 ) 46 47 // ValidateAuthenticationConfiguration validates a given AuthenticationConfiguration. 48 func ValidateAuthenticationConfiguration(c *api.AuthenticationConfiguration, disallowedIssuers []string) field.ErrorList { 49 root := field.NewPath("jwt") 50 var allErrs field.ErrorList 51 52 // We allow 0 authenticators in the authentication configuration. 53 // This allows us to support scenarios where the API server is initially set up without 54 // any authenticators and then authenticators are added later via dynamic config. 55 56 if len(c.JWT) > 64 { 57 allErrs = append(allErrs, field.TooMany(root, len(c.JWT), 64)) 58 return allErrs 59 } 60 61 seenIssuers := sets.New[string]() 62 seenDiscoveryURLs := sets.New[string]() 63 for i, a := range c.JWT { 64 fldPath := root.Index(i) 65 _, errs := validateJWTAuthenticator(a, fldPath, sets.New(disallowedIssuers...), utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthenticationConfiguration)) 66 allErrs = append(allErrs, errs...) 67 68 if seenIssuers.Has(a.Issuer.URL) { 69 allErrs = append(allErrs, field.Duplicate(fldPath.Child("issuer").Child("url"), a.Issuer.URL)) 70 } 71 seenIssuers.Insert(a.Issuer.URL) 72 73 if len(a.Issuer.DiscoveryURL) > 0 { 74 if seenDiscoveryURLs.Has(a.Issuer.DiscoveryURL) { 75 allErrs = append(allErrs, field.Duplicate(fldPath.Child("issuer").Child("discoveryURL"), a.Issuer.DiscoveryURL)) 76 } 77 seenDiscoveryURLs.Insert(a.Issuer.DiscoveryURL) 78 } 79 } 80 81 if c.Anonymous != nil { 82 if !utilfeature.DefaultFeatureGate.Enabled(features.AnonymousAuthConfigurableEndpoints) { 83 allErrs = append(allErrs, field.Forbidden(field.NewPath("anonymous"), "anonymous is not supported when AnonymousAuthConfigurableEnpoints feature gate is disabled")) 84 } 85 if !c.Anonymous.Enabled && len(c.Anonymous.Conditions) > 0 { 86 allErrs = append(allErrs, field.Invalid(field.NewPath("anonymous", "conditions"), c.Anonymous.Conditions, "enabled should be set to true when conditions are defined")) 87 } 88 } 89 90 return allErrs 91 } 92 93 // CompileAndValidateJWTAuthenticator validates a given JWTAuthenticator and returns a CELMapper with the compiled 94 // CEL expressions for claim mappings and validation rules. 95 // This is exported for use in oidc package. 96 func CompileAndValidateJWTAuthenticator(authenticator api.JWTAuthenticator, disallowedIssuers []string) (authenticationcel.CELMapper, field.ErrorList) { 97 return validateJWTAuthenticator(authenticator, nil, sets.New(disallowedIssuers...), utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthenticationConfiguration)) 98 } 99 100 func validateJWTAuthenticator(authenticator api.JWTAuthenticator, fldPath *field.Path, disallowedIssuers sets.Set[string], structuredAuthnFeatureEnabled bool) (authenticationcel.CELMapper, field.ErrorList) { 101 var allErrs field.ErrorList 102 103 // strictCost is set to true which enables the strict cost for CEL validation. 104 compiler := authenticationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) 105 state := &validationState{} 106 107 allErrs = append(allErrs, validateIssuer(authenticator.Issuer, disallowedIssuers, fldPath.Child("issuer"))...) 108 allErrs = append(allErrs, validateClaimValidationRules(compiler, state, authenticator.ClaimValidationRules, fldPath.Child("claimValidationRules"), structuredAuthnFeatureEnabled)...) 109 allErrs = append(allErrs, validateClaimMappings(compiler, state, authenticator.ClaimMappings, fldPath.Child("claimMappings"), structuredAuthnFeatureEnabled)...) 110 allErrs = append(allErrs, validateUserValidationRules(compiler, state, authenticator.UserValidationRules, fldPath.Child("userValidationRules"), structuredAuthnFeatureEnabled)...) 111 112 return state.mapper, allErrs 113 } 114 115 type validationState struct { 116 mapper authenticationcel.CELMapper 117 usesEmailClaim bool 118 usesEmailVerifiedClaim bool 119 } 120 121 func validateIssuer(issuer api.Issuer, disallowedIssuers sets.Set[string], fldPath *field.Path) field.ErrorList { 122 var allErrs field.ErrorList 123 124 allErrs = append(allErrs, validateIssuerURL(issuer.URL, disallowedIssuers, fldPath.Child("url"))...) 125 allErrs = append(allErrs, validateIssuerDiscoveryURL(issuer.URL, issuer.DiscoveryURL, fldPath.Child("discoveryURL"))...) 126 allErrs = append(allErrs, validateAudiences(issuer.Audiences, issuer.AudienceMatchPolicy, fldPath.Child("audiences"), fldPath.Child("audienceMatchPolicy"))...) 127 allErrs = append(allErrs, validateCertificateAuthority(issuer.CertificateAuthority, fldPath.Child("certificateAuthority"))...) 128 129 return allErrs 130 } 131 132 func validateIssuerURL(issuerURL string, disallowedIssuers sets.Set[string], fldPath *field.Path) field.ErrorList { 133 if len(issuerURL) == 0 { 134 return field.ErrorList{field.Required(fldPath, "URL is required")} 135 } 136 137 return validateURL(issuerURL, disallowedIssuers, fldPath) 138 } 139 140 func validateIssuerDiscoveryURL(issuerURL, issuerDiscoveryURL string, fldPath *field.Path) field.ErrorList { 141 var allErrs field.ErrorList 142 143 if len(issuerDiscoveryURL) == 0 { 144 return nil 145 } 146 147 if len(issuerURL) > 0 && strings.TrimRight(issuerURL, "/") == strings.TrimRight(issuerDiscoveryURL, "/") { 148 allErrs = append(allErrs, field.Invalid(fldPath, issuerDiscoveryURL, "discoveryURL must be different from URL")) 149 } 150 151 // issuerDiscoveryURL is not an issuer URL and does not need to validated against any set of disallowed issuers 152 allErrs = append(allErrs, validateURL(issuerDiscoveryURL, nil, fldPath)...) 153 return allErrs 154 } 155 156 func validateURL(issuerURL string, disallowedIssuers sets.Set[string], fldPath *field.Path) field.ErrorList { 157 var allErrs field.ErrorList 158 159 if disallowedIssuers.Has(issuerURL) { 160 allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, fmt.Sprintf("URL must not overlap with disallowed issuers: %s", sets.List(disallowedIssuers)))) 161 } 162 163 u, err := url.Parse(issuerURL) 164 if err != nil { 165 allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, err.Error())) 166 return allErrs 167 } 168 if u.Scheme != "https" { 169 allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, "URL scheme must be https")) 170 } 171 if u.User != nil { 172 allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, "URL must not contain a username or password")) 173 } 174 if len(u.RawQuery) > 0 { 175 allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, "URL must not contain a query")) 176 } 177 if len(u.Fragment) > 0 { 178 allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, "URL must not contain a fragment")) 179 } 180 181 return allErrs 182 } 183 184 func validateAudiences(audiences []string, audienceMatchPolicy api.AudienceMatchPolicyType, fldPath, audienceMatchPolicyFldPath *field.Path) field.ErrorList { 185 var allErrs field.ErrorList 186 187 if len(audiences) == 0 { 188 allErrs = append(allErrs, field.Required(fldPath, fmt.Sprintf(atLeastOneRequiredErrFmt, fldPath))) 189 return allErrs 190 } 191 192 seenAudiences := sets.NewString() 193 for i, audience := range audiences { 194 fldPath := fldPath.Index(i) 195 if len(audience) == 0 { 196 allErrs = append(allErrs, field.Required(fldPath, "audience can't be empty")) 197 } 198 if seenAudiences.Has(audience) { 199 allErrs = append(allErrs, field.Duplicate(fldPath, audience)) 200 } 201 seenAudiences.Insert(audience) 202 } 203 204 if len(audiences) > 1 && audienceMatchPolicy != api.AudienceMatchPolicyMatchAny { 205 allErrs = append(allErrs, field.Invalid(audienceMatchPolicyFldPath, audienceMatchPolicy, "audienceMatchPolicy must be MatchAny for multiple audiences")) 206 } 207 if len(audiences) == 1 && (len(audienceMatchPolicy) > 0 && audienceMatchPolicy != api.AudienceMatchPolicyMatchAny) { 208 allErrs = append(allErrs, field.Invalid(audienceMatchPolicyFldPath, audienceMatchPolicy, "audienceMatchPolicy must be empty or MatchAny for single audience")) 209 } 210 211 return allErrs 212 } 213 214 func validateCertificateAuthority(certificateAuthority string, fldPath *field.Path) field.ErrorList { 215 var allErrs field.ErrorList 216 217 if len(certificateAuthority) == 0 { 218 return allErrs 219 } 220 _, err := cert.NewPoolFromBytes([]byte(certificateAuthority)) 221 if err != nil { 222 allErrs = append(allErrs, field.Invalid(fldPath, "<omitted>", err.Error())) 223 } 224 225 return allErrs 226 } 227 228 func validateClaimValidationRules(compiler authenticationcel.Compiler, state *validationState, rules []api.ClaimValidationRule, fldPath *field.Path, structuredAuthnFeatureEnabled bool) field.ErrorList { 229 var allErrs field.ErrorList 230 231 seenClaims := sets.NewString() 232 seenExpressions := sets.NewString() 233 var compilationResults []authenticationcel.CompilationResult 234 235 for i, rule := range rules { 236 fldPath := fldPath.Index(i) 237 238 if len(rule.Expression) > 0 && !structuredAuthnFeatureEnabled { 239 allErrs = append(allErrs, field.Invalid(fldPath.Child("expression"), rule.Expression, "expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled")) 240 } 241 242 switch { 243 case len(rule.Claim) > 0 && len(rule.Expression) > 0: 244 allErrs = append(allErrs, field.Invalid(fldPath, rule.Claim, "claim and expression can't both be set")) 245 case len(rule.Claim) == 0 && len(rule.Expression) == 0: 246 allErrs = append(allErrs, field.Required(fldPath, "claim or expression is required")) 247 case len(rule.Claim) > 0: 248 if len(rule.Message) > 0 { 249 allErrs = append(allErrs, field.Invalid(fldPath.Child("message"), rule.Message, "message can't be set when claim is set")) 250 } 251 if seenClaims.Has(rule.Claim) { 252 allErrs = append(allErrs, field.Duplicate(fldPath.Child("claim"), rule.Claim)) 253 } 254 seenClaims.Insert(rule.Claim) 255 case len(rule.Expression) > 0: 256 if len(rule.RequiredValue) > 0 { 257 allErrs = append(allErrs, field.Invalid(fldPath.Child("requiredValue"), rule.RequiredValue, "requiredValue can't be set when expression is set")) 258 } 259 if seenExpressions.Has(rule.Expression) { 260 allErrs = append(allErrs, field.Duplicate(fldPath.Child("expression"), rule.Expression)) 261 continue 262 } 263 seenExpressions.Insert(rule.Expression) 264 265 compilationResult, err := compileClaimsCELExpression(compiler, &authenticationcel.ClaimValidationCondition{ 266 Expression: rule.Expression, 267 Message: rule.Message, 268 }, fldPath.Child("expression")) 269 270 if err != nil { 271 allErrs = append(allErrs, err) 272 continue 273 } 274 if compilationResult != nil { 275 compilationResults = append(compilationResults, *compilationResult) 276 } 277 } 278 } 279 280 if structuredAuthnFeatureEnabled && len(compilationResults) > 0 { 281 state.mapper.ClaimValidationRules = authenticationcel.NewClaimsMapper(compilationResults) 282 state.usesEmailVerifiedClaim = state.usesEmailVerifiedClaim || anyUsesEmailVerifiedClaim(compilationResults) 283 } 284 285 return allErrs 286 } 287 288 func validateClaimMappings(compiler authenticationcel.Compiler, state *validationState, m api.ClaimMappings, fldPath *field.Path, structuredAuthnFeatureEnabled bool) field.ErrorList { 289 var allErrs field.ErrorList 290 291 if !structuredAuthnFeatureEnabled { 292 if len(m.Username.Expression) > 0 { 293 allErrs = append(allErrs, field.Invalid(fldPath.Child("username").Child("expression"), m.Username.Expression, "expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled")) 294 } 295 if len(m.Groups.Expression) > 0 { 296 allErrs = append(allErrs, field.Invalid(fldPath.Child("groups").Child("expression"), m.Groups.Expression, "expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled")) 297 } 298 if len(m.UID.Claim) > 0 || len(m.UID.Expression) > 0 { 299 allErrs = append(allErrs, field.Invalid(fldPath.Child("uid"), "", "uid claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled")) 300 } 301 if len(m.Extra) > 0 { 302 allErrs = append(allErrs, field.Invalid(fldPath.Child("extra"), "", "extra claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled")) 303 } 304 } 305 306 compilationResult, err := validatePrefixClaimOrExpression(compiler, m.Username, fldPath.Child("username"), true) 307 if err != nil { 308 allErrs = append(allErrs, err...) 309 } else if compilationResult != nil && structuredAuthnFeatureEnabled { 310 state.usesEmailClaim = state.usesEmailClaim || usesEmailClaim(compilationResult.AST) 311 state.usesEmailVerifiedClaim = state.usesEmailVerifiedClaim || usesEmailVerifiedClaim(compilationResult.AST) 312 state.mapper.Username = authenticationcel.NewClaimsMapper([]authenticationcel.CompilationResult{*compilationResult}) 313 } 314 315 compilationResult, err = validatePrefixClaimOrExpression(compiler, m.Groups, fldPath.Child("groups"), false) 316 if err != nil { 317 allErrs = append(allErrs, err...) 318 } else if compilationResult != nil && structuredAuthnFeatureEnabled { 319 state.mapper.Groups = authenticationcel.NewClaimsMapper([]authenticationcel.CompilationResult{*compilationResult}) 320 } 321 322 switch { 323 case len(m.UID.Claim) > 0 && len(m.UID.Expression) > 0: 324 allErrs = append(allErrs, field.Invalid(fldPath.Child("uid"), "", "claim and expression can't both be set")) 325 case len(m.UID.Expression) > 0: 326 compilationResult, err := compileClaimsCELExpression(compiler, &authenticationcel.ClaimMappingExpression{ 327 Expression: m.UID.Expression, 328 }, fldPath.Child("uid").Child("expression")) 329 330 if err != nil { 331 allErrs = append(allErrs, err) 332 } else if structuredAuthnFeatureEnabled && compilationResult != nil { 333 state.mapper.UID = authenticationcel.NewClaimsMapper([]authenticationcel.CompilationResult{*compilationResult}) 334 } 335 } 336 337 var extraCompilationResults []authenticationcel.CompilationResult 338 seenExtraKeys := sets.NewString() 339 340 for i, mapping := range m.Extra { 341 fldPath := fldPath.Child("extra").Index(i) 342 // Key should be namespaced to the authenticator or authenticator/authorizer pair making use of them. 343 // For instance: "example.org/foo" instead of "foo". 344 // xref: https://github.com/kubernetes/kubernetes/blob/3825e206cb162a7ad7431a5bdf6a065ae8422cf7/staging/src/k8s.io/apiserver/pkg/authentication/user/user.go#L31-L41 345 // IsDomainPrefixedPath checks for non-empty key and that the key is prefixed with a domain name. 346 allErrs = append(allErrs, utilvalidation.IsDomainPrefixedPath(fldPath.Child("key"), mapping.Key)...) 347 if mapping.Key != strings.ToLower(mapping.Key) { 348 allErrs = append(allErrs, field.Invalid(fldPath.Child("key"), mapping.Key, "key must be lowercase")) 349 } 350 if seenExtraKeys.Has(mapping.Key) { 351 allErrs = append(allErrs, field.Duplicate(fldPath.Child("key"), mapping.Key)) 352 continue 353 } 354 seenExtraKeys.Insert(mapping.Key) 355 356 if len(mapping.ValueExpression) == 0 { 357 allErrs = append(allErrs, field.Required(fldPath.Child("valueExpression"), "valueExpression is required")) 358 continue 359 } 360 361 compilationResult, err := compileClaimsCELExpression(compiler, &authenticationcel.ExtraMappingExpression{ 362 Key: mapping.Key, 363 Expression: mapping.ValueExpression, 364 }, fldPath.Child("valueExpression")) 365 366 if err != nil { 367 allErrs = append(allErrs, err) 368 continue 369 } 370 371 if compilationResult != nil { 372 extraCompilationResults = append(extraCompilationResults, *compilationResult) 373 } 374 } 375 376 if structuredAuthnFeatureEnabled && len(extraCompilationResults) > 0 { 377 state.mapper.Extra = authenticationcel.NewClaimsMapper(extraCompilationResults) 378 state.usesEmailVerifiedClaim = state.usesEmailVerifiedClaim || anyUsesEmailVerifiedClaim(extraCompilationResults) 379 } 380 381 if structuredAuthnFeatureEnabled && state.usesEmailClaim && !state.usesEmailVerifiedClaim { 382 allErrs = append(allErrs, field.Invalid(fldPath.Child("username", "expression"), m.Username.Expression, 383 "claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression")) 384 } 385 386 return allErrs 387 } 388 389 func usesEmailClaim(ast *celgo.Ast) bool { 390 return hasSelectExp(ast.Expr(), "claims", "email") 391 } 392 393 func anyUsesEmailVerifiedClaim(results []authenticationcel.CompilationResult) bool { 394 for _, result := range results { 395 if usesEmailVerifiedClaim(result.AST) { 396 return true 397 } 398 } 399 return false 400 } 401 402 func usesEmailVerifiedClaim(ast *celgo.Ast) bool { 403 return hasSelectExp(ast.Expr(), "claims", "email_verified") 404 } 405 406 func hasSelectExp(exp *exprpb.Expr, operand, field string) bool { 407 if exp == nil { 408 return false 409 } 410 switch e := exp.ExprKind.(type) { 411 case *exprpb.Expr_ConstExpr, 412 *exprpb.Expr_IdentExpr: 413 return false 414 case *exprpb.Expr_SelectExpr: 415 s := e.SelectExpr 416 if s == nil { 417 return false 418 } 419 if isIdentOperand(s.Operand, operand) && s.Field == field { 420 return true 421 } 422 return hasSelectExp(s.Operand, operand, field) 423 case *exprpb.Expr_CallExpr: 424 c := e.CallExpr 425 if c == nil { 426 return false 427 } 428 if c.Target == nil && c.Function == operators.OptSelect && len(c.Args) == 2 && 429 isIdentOperand(c.Args[0], operand) && isConstField(c.Args[1], field) { 430 return true 431 } 432 for _, arg := range c.Args { 433 if hasSelectExp(arg, operand, field) { 434 return true 435 } 436 } 437 return hasSelectExp(c.Target, operand, field) 438 case *exprpb.Expr_ListExpr: 439 l := e.ListExpr 440 if l == nil { 441 return false 442 } 443 for _, element := range l.Elements { 444 if hasSelectExp(element, operand, field) { 445 return true 446 } 447 } 448 return false 449 case *exprpb.Expr_StructExpr: 450 s := e.StructExpr 451 if s == nil { 452 return false 453 } 454 for _, entry := range s.Entries { 455 if hasSelectExp(entry.GetMapKey(), operand, field) { 456 return true 457 } 458 if hasSelectExp(entry.Value, operand, field) { 459 return true 460 } 461 } 462 return false 463 case *exprpb.Expr_ComprehensionExpr: 464 c := e.ComprehensionExpr 465 if c == nil { 466 return false 467 } 468 return hasSelectExp(c.IterRange, operand, field) || 469 hasSelectExp(c.AccuInit, operand, field) || 470 hasSelectExp(c.LoopCondition, operand, field) || 471 hasSelectExp(c.LoopStep, operand, field) || 472 hasSelectExp(c.Result, operand, field) 473 default: 474 return false 475 } 476 } 477 478 func isIdentOperand(exp *exprpb.Expr, operand string) bool { 479 if len(operand) == 0 { 480 return false // sanity check against default values 481 } 482 id := exp.GetIdentExpr() // does not panic even if exp is nil 483 return id != nil && id.Name == operand 484 } 485 486 func isConstField(exp *exprpb.Expr, field string) bool { 487 if len(field) == 0 { 488 return false // sanity check against default values 489 } 490 c := exp.GetConstExpr() // does not panic even if exp is nil 491 return c != nil && c.GetStringValue() == field // does not panic even if c is not a string 492 } 493 494 func validatePrefixClaimOrExpression(compiler authenticationcel.Compiler, mapping api.PrefixedClaimOrExpression, fldPath *field.Path, claimOrExpressionRequired bool) (*authenticationcel.CompilationResult, field.ErrorList) { 495 var allErrs field.ErrorList 496 497 var compilationResult *authenticationcel.CompilationResult 498 switch { 499 case len(mapping.Expression) > 0 && len(mapping.Claim) > 0: 500 allErrs = append(allErrs, field.Invalid(fldPath, "", "claim and expression can't both be set")) 501 case len(mapping.Expression) == 0 && len(mapping.Claim) == 0 && claimOrExpressionRequired: 502 allErrs = append(allErrs, field.Required(fldPath, "claim or expression is required")) 503 case len(mapping.Expression) > 0: 504 var err *field.Error 505 506 if mapping.Prefix != nil { 507 allErrs = append(allErrs, field.Invalid(fldPath.Child("prefix"), *mapping.Prefix, "prefix can't be set when expression is set")) 508 } 509 compilationResult, err = compileClaimsCELExpression(compiler, &authenticationcel.ClaimMappingExpression{ 510 Expression: mapping.Expression, 511 }, fldPath.Child("expression")) 512 513 if err != nil { 514 allErrs = append(allErrs, err) 515 } 516 517 case len(mapping.Claim) > 0: 518 if mapping.Prefix == nil { 519 allErrs = append(allErrs, field.Required(fldPath.Child("prefix"), "prefix is required when claim is set. It can be set to an empty string to disable prefixing")) 520 } 521 } 522 523 return compilationResult, allErrs 524 } 525 526 func validateUserValidationRules(compiler authenticationcel.Compiler, state *validationState, rules []api.UserValidationRule, fldPath *field.Path, structuredAuthnFeatureEnabled bool) field.ErrorList { 527 var allErrs field.ErrorList 528 var compilationResults []authenticationcel.CompilationResult 529 530 if len(rules) > 0 && !structuredAuthnFeatureEnabled { 531 allErrs = append(allErrs, field.Invalid(fldPath, "", "user validation rules are not supported when StructuredAuthenticationConfiguration feature gate is disabled")) 532 } 533 534 seenExpressions := sets.NewString() 535 for i, rule := range rules { 536 fldPath := fldPath.Index(i) 537 538 if len(rule.Expression) == 0 { 539 allErrs = append(allErrs, field.Required(fldPath.Child("expression"), "expression is required")) 540 continue 541 } 542 543 if seenExpressions.Has(rule.Expression) { 544 allErrs = append(allErrs, field.Duplicate(fldPath.Child("expression"), rule.Expression)) 545 continue 546 } 547 seenExpressions.Insert(rule.Expression) 548 549 compilationResult, err := compileUserCELExpression(compiler, &authenticationcel.UserValidationCondition{ 550 Expression: rule.Expression, 551 Message: rule.Message, 552 }, fldPath.Child("expression")) 553 554 if err != nil { 555 allErrs = append(allErrs, err) 556 continue 557 } 558 559 if compilationResult != nil { 560 compilationResults = append(compilationResults, *compilationResult) 561 } 562 } 563 564 if structuredAuthnFeatureEnabled && len(compilationResults) > 0 { 565 state.mapper.UserValidationRules = authenticationcel.NewUserMapper(compilationResults) 566 } 567 568 return allErrs 569 } 570 571 func compileClaimsCELExpression(compiler authenticationcel.Compiler, expression authenticationcel.ExpressionAccessor, fldPath *field.Path) (*authenticationcel.CompilationResult, *field.Error) { 572 compilationResult, err := compiler.CompileClaimsExpression(expression) 573 if err != nil { 574 return nil, convertCELErrorToValidationError(fldPath, expression.GetExpression(), err) 575 } 576 return &compilationResult, nil 577 } 578 579 func compileUserCELExpression(compiler authenticationcel.Compiler, expression authenticationcel.ExpressionAccessor, fldPath *field.Path) (*authenticationcel.CompilationResult, *field.Error) { 580 compilationResult, err := compiler.CompileUserExpression(expression) 581 if err != nil { 582 return nil, convertCELErrorToValidationError(fldPath, expression.GetExpression(), err) 583 } 584 return &compilationResult, nil 585 } 586 587 // ValidateAuthorizationConfiguration validates a given AuthorizationConfiguration. 588 func ValidateAuthorizationConfiguration(fldPath *field.Path, c *api.AuthorizationConfiguration, knownTypes sets.String, repeatableTypes sets.String) field.ErrorList { 589 allErrs := field.ErrorList{} 590 591 if len(c.Authorizers) == 0 { 592 allErrs = append(allErrs, field.Required(fldPath.Child("authorizers"), "at least one authorization mode must be defined")) 593 } 594 595 seenAuthorizerTypes := sets.NewString() 596 seenAuthorizerNames := sets.NewString() 597 for i, a := range c.Authorizers { 598 fldPath := fldPath.Child("authorizers").Index(i) 599 aType := string(a.Type) 600 if aType == "" { 601 allErrs = append(allErrs, field.Required(fldPath.Child("type"), "")) 602 continue 603 } 604 if !knownTypes.Has(aType) { 605 allErrs = append(allErrs, field.NotSupported(fldPath.Child("type"), aType, knownTypes.List())) 606 continue 607 } 608 if seenAuthorizerTypes.Has(aType) && !repeatableTypes.Has(aType) { 609 allErrs = append(allErrs, field.Duplicate(fldPath.Child("type"), aType)) 610 continue 611 } 612 seenAuthorizerTypes.Insert(aType) 613 614 if len(a.Name) == 0 { 615 allErrs = append(allErrs, field.Required(fldPath.Child("name"), "")) 616 } else if seenAuthorizerNames.Has(a.Name) { 617 allErrs = append(allErrs, field.Duplicate(fldPath.Child("name"), a.Name)) 618 } else if errs := utilvalidation.IsDNS1123Subdomain(a.Name); len(errs) != 0 { 619 allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), a.Name, fmt.Sprintf("authorizer name is invalid: %s", strings.Join(errs, ", ")))) 620 } 621 seenAuthorizerNames.Insert(a.Name) 622 623 switch a.Type { 624 case api.TypeWebhook: 625 if a.Webhook == nil { 626 allErrs = append(allErrs, field.Required(fldPath.Child("webhook"), "required when type=Webhook")) 627 continue 628 } 629 allErrs = append(allErrs, ValidateWebhookConfiguration(fldPath, a.Webhook)...) 630 default: 631 if a.Webhook != nil { 632 allErrs = append(allErrs, field.Invalid(fldPath.Child("webhook"), "non-null", "may only be specified when type=Webhook")) 633 } 634 } 635 } 636 637 return allErrs 638 } 639 640 func ValidateWebhookConfiguration(fldPath *field.Path, c *api.WebhookConfiguration) field.ErrorList { 641 allErrs := field.ErrorList{} 642 643 if c.Timeout.Duration == 0 { 644 allErrs = append(allErrs, field.Required(fldPath.Child("timeout"), "")) 645 } else if c.Timeout.Duration > 30*time.Second || c.Timeout.Duration < 0 { 646 allErrs = append(allErrs, field.Invalid(fldPath.Child("timeout"), c.Timeout.Duration.String(), "must be > 0s and <= 30s")) 647 } 648 649 if c.AuthorizedTTL.Duration == 0 { 650 allErrs = append(allErrs, field.Required(fldPath.Child("authorizedTTL"), "")) 651 } else if c.AuthorizedTTL.Duration < 0 { 652 allErrs = append(allErrs, field.Invalid(fldPath.Child("authorizedTTL"), c.AuthorizedTTL.Duration.String(), "must be > 0s")) 653 } 654 655 if c.UnauthorizedTTL.Duration == 0 { 656 allErrs = append(allErrs, field.Required(fldPath.Child("unauthorizedTTL"), "")) 657 } else if c.UnauthorizedTTL.Duration < 0 { 658 allErrs = append(allErrs, field.Invalid(fldPath.Child("unauthorizedTTL"), c.UnauthorizedTTL.Duration.String(), "must be > 0s")) 659 } 660 661 switch c.SubjectAccessReviewVersion { 662 case "": 663 allErrs = append(allErrs, field.Required(fldPath.Child("subjectAccessReviewVersion"), "")) 664 case "v1": 665 _ = &v1.SubjectAccessReview{} 666 case "v1beta1": 667 _ = &v1beta1.SubjectAccessReview{} 668 default: 669 allErrs = append(allErrs, field.NotSupported(fldPath.Child("subjectAccessReviewVersion"), c.SubjectAccessReviewVersion, []string{"v1", "v1beta1"})) 670 } 671 672 switch c.MatchConditionSubjectAccessReviewVersion { 673 case "": 674 if len(c.MatchConditions) > 0 { 675 allErrs = append(allErrs, field.Required(fldPath.Child("matchConditionSubjectAccessReviewVersion"), "required if match conditions are specified")) 676 } 677 case "v1": 678 _ = &v1.SubjectAccessReview{} 679 default: 680 allErrs = append(allErrs, field.NotSupported(fldPath.Child("matchConditionSubjectAccessReviewVersion"), c.MatchConditionSubjectAccessReviewVersion, []string{"v1"})) 681 } 682 683 switch c.FailurePolicy { 684 case "": 685 allErrs = append(allErrs, field.Required(fldPath.Child("failurePolicy"), "")) 686 case api.FailurePolicyNoOpinion, api.FailurePolicyDeny: 687 default: 688 allErrs = append(allErrs, field.NotSupported(fldPath.Child("failurePolicy"), c.FailurePolicy, []string{"NoOpinion", "Deny"})) 689 } 690 691 switch c.ConnectionInfo.Type { 692 case "": 693 allErrs = append(allErrs, field.Required(fldPath.Child("connectionInfo", "type"), "")) 694 case api.AuthorizationWebhookConnectionInfoTypeInCluster: 695 if c.ConnectionInfo.KubeConfigFile != nil { 696 allErrs = append(allErrs, field.Invalid(fldPath.Child("connectionInfo", "kubeConfigFile"), *c.ConnectionInfo.KubeConfigFile, "can only be set when type=KubeConfigFile")) 697 } 698 case api.AuthorizationWebhookConnectionInfoTypeKubeConfigFile: 699 if c.ConnectionInfo.KubeConfigFile == nil || *c.ConnectionInfo.KubeConfigFile == "" { 700 allErrs = append(allErrs, field.Required(fldPath.Child("connectionInfo", "kubeConfigFile"), "")) 701 } else if !filepath.IsAbs(*c.ConnectionInfo.KubeConfigFile) { 702 allErrs = append(allErrs, field.Invalid(fldPath.Child("connectionInfo", "kubeConfigFile"), *c.ConnectionInfo.KubeConfigFile, "must be an absolute path")) 703 } else if info, err := os.Stat(*c.ConnectionInfo.KubeConfigFile); err != nil { 704 allErrs = append(allErrs, field.Invalid(fldPath.Child("connectionInfo", "kubeConfigFile"), *c.ConnectionInfo.KubeConfigFile, fmt.Sprintf("error loading file: %v", err))) 705 } else if !info.Mode().IsRegular() { 706 allErrs = append(allErrs, field.Invalid(fldPath.Child("connectionInfo", "kubeConfigFile"), *c.ConnectionInfo.KubeConfigFile, "must be a regular file")) 707 } 708 default: 709 allErrs = append(allErrs, field.NotSupported(fldPath.Child("connectionInfo", "type"), c.ConnectionInfo, []string{api.AuthorizationWebhookConnectionInfoTypeInCluster, api.AuthorizationWebhookConnectionInfoTypeKubeConfigFile})) 710 } 711 712 _, errs := compileMatchConditions(c.MatchConditions, fldPath, utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthorizationConfiguration)) 713 allErrs = append(allErrs, errs...) 714 715 return allErrs 716 } 717 718 // ValidateAndCompileMatchConditions validates a given webhook's matchConditions. 719 // This is exported for use in authz package. 720 func ValidateAndCompileMatchConditions(matchConditions []api.WebhookMatchCondition) (*authorizationcel.CELMatcher, field.ErrorList) { 721 return compileMatchConditions(matchConditions, nil, utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthorizationConfiguration)) 722 } 723 724 func compileMatchConditions(matchConditions []api.WebhookMatchCondition, fldPath *field.Path, structuredAuthzFeatureEnabled bool) (*authorizationcel.CELMatcher, field.ErrorList) { 725 var allErrs field.ErrorList 726 // should fail when match conditions are used without feature enabled 727 if len(matchConditions) > 0 && !structuredAuthzFeatureEnabled { 728 allErrs = append(allErrs, field.Invalid(fldPath.Child("matchConditions"), "", "matchConditions are not supported when StructuredAuthorizationConfiguration feature gate is disabled")) 729 } 730 if len(matchConditions) > 64 { 731 allErrs = append(allErrs, field.TooMany(fldPath.Child("matchConditions"), len(matchConditions), 64)) 732 return nil, allErrs 733 } 734 735 // strictCost is set to true which enables the strict cost for CEL validation. 736 compiler := authorizationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) 737 seenExpressions := sets.NewString() 738 var compilationResults []authorizationcel.CompilationResult 739 var usesFieldSelector, usesLabelSelector bool 740 741 for i, condition := range matchConditions { 742 fldPath := fldPath.Child("matchConditions").Index(i).Child("expression") 743 if len(strings.TrimSpace(condition.Expression)) == 0 { 744 allErrs = append(allErrs, field.Required(fldPath, "")) 745 continue 746 } 747 if seenExpressions.Has(condition.Expression) { 748 allErrs = append(allErrs, field.Duplicate(fldPath, condition.Expression)) 749 continue 750 } 751 seenExpressions.Insert(condition.Expression) 752 compilationResult, err := compileMatchConditionsExpression(fldPath, compiler, condition.Expression) 753 if err != nil { 754 allErrs = append(allErrs, err) 755 continue 756 } 757 compilationResults = append(compilationResults, compilationResult) 758 usesFieldSelector = usesFieldSelector || compilationResult.UsesFieldSelector 759 usesLabelSelector = usesLabelSelector || compilationResult.UsesLabelSelector 760 } 761 if len(compilationResults) == 0 { 762 return nil, allErrs 763 } 764 return &authorizationcel.CELMatcher{ 765 CompilationResults: compilationResults, 766 UsesFieldSelector: usesFieldSelector, 767 UsesLabelSelector: usesLabelSelector, 768 }, allErrs 769 } 770 771 func compileMatchConditionsExpression(fldPath *field.Path, compiler authorizationcel.Compiler, expression string) (authorizationcel.CompilationResult, *field.Error) { 772 authzExpression := &authorizationcel.SubjectAccessReviewMatchCondition{ 773 Expression: expression, 774 } 775 compilationResult, err := compiler.CompileCELExpression(authzExpression) 776 if err != nil { 777 return compilationResult, convertCELErrorToValidationError(fldPath, authzExpression.GetExpression(), err) 778 } 779 return compilationResult, nil 780 } 781 782 func convertCELErrorToValidationError(fldPath *field.Path, expression string, err error) *field.Error { 783 var celErr *cel.Error 784 if errors.As(err, &celErr) { 785 switch celErr.Type { 786 case cel.ErrorTypeRequired: 787 return field.Required(fldPath, celErr.Detail) 788 case cel.ErrorTypeInvalid: 789 return field.Invalid(fldPath, expression, celErr.Detail) 790 default: 791 return field.InternalError(fldPath, celErr) 792 } 793 } 794 return field.InternalError(fldPath, fmt.Errorf("error is not cel error: %w", err)) 795 }