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  }