apiserver/authconfig: wire CEL compiler through lower layers to allow sharing

Signed-off-by: Dr. Stefan Schimanski <stefan.schimanski@gmail.com>

Kubernetes-commit: 4024390d8c8a19056ab7ced95eef5cce43c8096d
This commit is contained in:
Dr. Stefan Schimanski 2024-09-20 12:34:08 +02:00 committed by Kubernetes Publisher
parent b907ccabbe
commit 4b46916a7b
12 changed files with 91 additions and 73 deletions

View File

@ -38,14 +38,13 @@ import (
authenticationcel "k8s.io/apiserver/pkg/authentication/cel"
authorizationcel "k8s.io/apiserver/pkg/authorization/cel"
"k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/util/cert"
)
// ValidateAuthenticationConfiguration validates a given AuthenticationConfiguration.
func ValidateAuthenticationConfiguration(c *api.AuthenticationConfiguration, disallowedIssuers []string) field.ErrorList {
func ValidateAuthenticationConfiguration(compiler authenticationcel.Compiler, c *api.AuthenticationConfiguration, disallowedIssuers []string) field.ErrorList {
root := field.NewPath("jwt")
var allErrs field.ErrorList
@ -62,7 +61,7 @@ func ValidateAuthenticationConfiguration(c *api.AuthenticationConfiguration, dis
seenDiscoveryURLs := sets.New[string]()
for i, a := range c.JWT {
fldPath := root.Index(i)
_, errs := validateJWTAuthenticator(a, fldPath, sets.New(disallowedIssuers...), utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthenticationConfiguration))
_, errs := validateJWTAuthenticator(compiler, a, fldPath, sets.New(disallowedIssuers...), utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthenticationConfiguration))
allErrs = append(allErrs, errs...)
if seenIssuers.Has(a.Issuer.URL) {
@ -93,15 +92,13 @@ func ValidateAuthenticationConfiguration(c *api.AuthenticationConfiguration, dis
// CompileAndValidateJWTAuthenticator validates a given JWTAuthenticator and returns a CELMapper with the compiled
// CEL expressions for claim mappings and validation rules.
// This is exported for use in oidc package.
func CompileAndValidateJWTAuthenticator(authenticator api.JWTAuthenticator, disallowedIssuers []string) (authenticationcel.CELMapper, field.ErrorList) {
return validateJWTAuthenticator(authenticator, nil, sets.New(disallowedIssuers...), utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthenticationConfiguration))
func CompileAndValidateJWTAuthenticator(compiler authenticationcel.Compiler, authenticator api.JWTAuthenticator, disallowedIssuers []string) (authenticationcel.CELMapper, field.ErrorList) {
return validateJWTAuthenticator(compiler, authenticator, nil, sets.New(disallowedIssuers...), utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthenticationConfiguration))
}
func validateJWTAuthenticator(authenticator api.JWTAuthenticator, fldPath *field.Path, disallowedIssuers sets.Set[string], structuredAuthnFeatureEnabled bool) (authenticationcel.CELMapper, field.ErrorList) {
func validateJWTAuthenticator(compiler authenticationcel.Compiler, authenticator api.JWTAuthenticator, fldPath *field.Path, disallowedIssuers sets.Set[string], structuredAuthnFeatureEnabled bool) (authenticationcel.CELMapper, field.ErrorList) {
var allErrs field.ErrorList
// strictCost is set to true which enables the strict cost for CEL validation.
compiler := authenticationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
state := &validationState{}
allErrs = append(allErrs, validateIssuer(authenticator.Issuer, disallowedIssuers, fldPath.Child("issuer"), structuredAuthnFeatureEnabled)...)
@ -616,7 +613,7 @@ func compileUserCELExpression(compiler authenticationcel.Compiler, expression au
}
// ValidateAuthorizationConfiguration validates a given AuthorizationConfiguration.
func ValidateAuthorizationConfiguration(fldPath *field.Path, c *api.AuthorizationConfiguration, knownTypes sets.String, repeatableTypes sets.String) field.ErrorList {
func ValidateAuthorizationConfiguration(compiler authorizationcel.Compiler, fldPath *field.Path, c *api.AuthorizationConfiguration, knownTypes sets.String, repeatableTypes sets.String) field.ErrorList {
allErrs := field.ErrorList{}
if len(c.Authorizers) == 0 {
@ -657,7 +654,7 @@ func ValidateAuthorizationConfiguration(fldPath *field.Path, c *api.Authorizatio
allErrs = append(allErrs, field.Required(fldPath.Child("webhook"), "required when type=Webhook"))
continue
}
allErrs = append(allErrs, ValidateWebhookConfiguration(fldPath, a.Webhook)...)
allErrs = append(allErrs, ValidateWebhookConfiguration(compiler, fldPath, a.Webhook)...)
default:
if a.Webhook != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("webhook"), "non-null", "may only be specified when type=Webhook"))
@ -668,7 +665,7 @@ func ValidateAuthorizationConfiguration(fldPath *field.Path, c *api.Authorizatio
return allErrs
}
func ValidateWebhookConfiguration(fldPath *field.Path, c *api.WebhookConfiguration) field.ErrorList {
func ValidateWebhookConfiguration(compiler authorizationcel.Compiler, fldPath *field.Path, c *api.WebhookConfiguration) field.ErrorList {
allErrs := field.ErrorList{}
if c.Timeout.Duration == 0 {
@ -740,7 +737,7 @@ func ValidateWebhookConfiguration(fldPath *field.Path, c *api.WebhookConfigurati
allErrs = append(allErrs, field.NotSupported(fldPath.Child("connectionInfo", "type"), c.ConnectionInfo, []string{api.AuthorizationWebhookConnectionInfoTypeInCluster, api.AuthorizationWebhookConnectionInfoTypeKubeConfigFile}))
}
_, errs := compileMatchConditions(c.MatchConditions, fldPath, utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthorizationConfiguration))
_, errs := compileMatchConditions(compiler, c.MatchConditions, fldPath, utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthorizationConfiguration))
allErrs = append(allErrs, errs...)
return allErrs
@ -748,11 +745,11 @@ func ValidateWebhookConfiguration(fldPath *field.Path, c *api.WebhookConfigurati
// ValidateAndCompileMatchConditions validates a given webhook's matchConditions.
// This is exported for use in authz package.
func ValidateAndCompileMatchConditions(matchConditions []api.WebhookMatchCondition) (*authorizationcel.CELMatcher, field.ErrorList) {
return compileMatchConditions(matchConditions, nil, utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthorizationConfiguration))
func ValidateAndCompileMatchConditions(compiler authorizationcel.Compiler, matchConditions []api.WebhookMatchCondition) (*authorizationcel.CELMatcher, field.ErrorList) {
return compileMatchConditions(compiler, matchConditions, nil, utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthorizationConfiguration))
}
func compileMatchConditions(matchConditions []api.WebhookMatchCondition, fldPath *field.Path, structuredAuthzFeatureEnabled bool) (*authorizationcel.CELMatcher, field.ErrorList) {
func compileMatchConditions(compiler authorizationcel.Compiler, matchConditions []api.WebhookMatchCondition, fldPath *field.Path, structuredAuthzFeatureEnabled bool) (*authorizationcel.CELMatcher, field.ErrorList) {
var allErrs field.ErrorList
// should fail when match conditions are used without feature enabled
if len(matchConditions) > 0 && !structuredAuthzFeatureEnabled {
@ -763,8 +760,6 @@ func compileMatchConditions(matchConditions []api.WebhookMatchCondition, fldPath
return nil, allErrs
}
// strictCost is set to true which enables the strict cost for CEL validation.
compiler := authorizationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
seenExpressions := sets.NewString()
var compilationResults []authorizationcel.CompilationResult
var usesFieldSelector, usesLabelSelector bool

View File

@ -34,7 +34,7 @@ import (
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/apiserver/pkg/apis/apiserver"
authenticationcel "k8s.io/apiserver/pkg/authentication/cel"
"k8s.io/apiserver/pkg/cel/environment"
authorizationcel "k8s.io/apiserver/pkg/authorization/cel"
"k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
certutil "k8s.io/client-go/util/cert"
@ -42,10 +42,6 @@ import (
"k8s.io/utils/pointer"
)
var (
compiler = authenticationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
)
func TestValidateAuthenticationConfiguration(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)
@ -619,7 +615,7 @@ func TestValidateAuthenticationConfiguration(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got := ValidateAuthenticationConfiguration(tt.in, tt.disallowedIssuers).ToAggregate()
got := ValidateAuthenticationConfiguration(authenticationcel.NewDefaultCompiler(), tt.in, tt.disallowedIssuers).ToAggregate()
if d := cmp.Diff(tt.want, errString(got)); d != "" {
t.Fatalf("AuthenticationConfiguration validation mismatch (-want +got):\n%s", d)
}
@ -1044,7 +1040,7 @@ func TestValidateClaimValidationRules(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
state := &validationState{}
got := validateClaimValidationRules(compiler, state, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate()
got := validateClaimValidationRules(authenticationcel.NewDefaultCompiler(), state, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate()
if d := cmp.Diff(tt.want, errString(got)); d != "" {
t.Fatalf("ClaimValidationRules validation mismatch (-want +got):\n%s", d)
}
@ -1572,7 +1568,7 @@ func TestValidateClaimMappings(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
state := &validationState{usesEmailVerifiedClaim: tt.usesEmailVerifiedClaim}
got := validateClaimMappings(compiler, state, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate()
got := validateClaimMappings(authenticationcel.NewDefaultCompiler(), state, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate()
if d := cmp.Diff(tt.want, errString(got)); d != "" {
fmt.Println(errString(got))
t.Fatalf("ClaimMappings validation mismatch (-want +got):\n%s", d)
@ -1661,7 +1657,7 @@ func TestValidateUserValidationRules(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
state := &validationState{}
got := validateUserValidationRules(compiler, state, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate()
got := validateUserValidationRules(authenticationcel.NewDefaultCompiler(), state, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate()
if d := cmp.Diff(tt.want, errString(got)); d != "" {
t.Fatalf("UserValidationRules validation mismatch (-want +got):\n%s", d)
}
@ -2458,7 +2454,7 @@ func TestValidateAuthorizationConfiguration(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
errList := ValidateAuthorizationConfiguration(nil, &test.configuration, test.knownTypes, test.repeatableTypes)
errList := ValidateAuthorizationConfiguration(authorizationcel.NewDefaultCompiler(), nil, &test.configuration, test.knownTypes, test.repeatableTypes)
if len(errList) != len(test.expectedErrList) {
t.Errorf("expected %d errs, got %d, errors %v", len(test.expectedErrList), len(errList), errList)
}
@ -2568,7 +2564,7 @@ func TestValidateAndCompileMatchConditions(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, tt.featureEnabled)
celMatcher, errList := ValidateAndCompileMatchConditions(tt.matchConditions)
celMatcher, errList := ValidateAndCompileMatchConditions(authorizationcel.NewDefaultCompiler(), tt.matchConditions)
if len(tt.expectedErr) == 0 && len(tt.matchConditions) > 0 && len(errList) == 0 && celMatcher == nil {
t.Errorf("celMatcher should not be nil when there are matchCondition and no error returned")
}

View File

@ -39,6 +39,12 @@ type compiler struct {
varEnvs map[string]*environment.EnvSet
}
// NewDefaultCompiler returns a new Compiler following the default compatibility version.
// Note: the compiler construction depends on feature gates and the compatibility version to be initialized.
func NewDefaultCompiler() Compiler {
return NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
}
// NewCompiler returns a new Compiler.
func NewCompiler(env *environment.EnvSet) Compiler {
return &compiler{

View File

@ -23,7 +23,6 @@ import (
authenticationv1 "k8s.io/api/authentication/v1"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/environment"
)
func TestCompileClaimsExpression(t *testing.T) {
@ -57,12 +56,10 @@ func TestCompileClaimsExpression(t *testing.T) {
},
}
compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
for _, expressionAccessor := range tc.expressionAccessors {
_, err := compiler.CompileClaimsExpression(expressionAccessor)
_, err := NewDefaultCompiler().CompileClaimsExpression(expressionAccessor)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
@ -86,12 +83,10 @@ func TestCompileUserExpression(t *testing.T) {
},
}
compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
for _, expressionAccessor := range tc.expressionAccessors {
_, err := compiler.CompileUserExpression(expressionAccessor)
_, err := NewDefaultCompiler().CompileUserExpression(expressionAccessor)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
@ -135,12 +130,10 @@ func TestCompileClaimsExpressionError(t *testing.T) {
},
}
compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
for _, expressionAccessor := range tc.expressionAccessors {
_, err := compiler.CompileClaimsExpression(expressionAccessor)
_, err := NewDefaultCompiler().CompileClaimsExpression(expressionAccessor)
if err == nil {
t.Errorf("expected error but got nil")
}
@ -205,12 +198,10 @@ func TestCompileUserExpressionError(t *testing.T) {
},
}
compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
for _, expressionAccessor := range tc.expressionAccessors {
_, err := compiler.CompileUserExpression(expressionAccessor)
_, err := NewDefaultCompiler().CompileUserExpression(expressionAccessor)
if err == nil {
t.Errorf("expected error but got nil")
}

View File

@ -22,6 +22,7 @@ import (
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/authorization/authorizer"
authorizationcel "k8s.io/apiserver/pkg/authorization/cel"
"k8s.io/apiserver/plugin/pkg/authorizer/webhook"
authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1"
)
@ -31,6 +32,9 @@ import (
type DelegatingAuthorizerConfig struct {
SubjectAccessReviewClient authorizationclient.AuthorizationV1Interface
// Compiler is the CEL compiler to use for evaluating policies. If nil, a default compiler will be used.
Compiler authorizationcel.Compiler
// AllowCacheTTL is the length of time that a successful authorization response will be cached
AllowCacheTTL time.Duration
@ -48,6 +52,10 @@ func (c DelegatingAuthorizerConfig) New() (authorizer.Authorizer, error) {
if c.WebhookRetryBackoff == nil {
return nil, errors.New("retry backoff parameters for delegating authorization webhook has not been specified")
}
compiler := c.Compiler
if compiler == nil {
compiler = authorizationcel.NewDefaultCompiler()
}
return webhook.NewFromInterface(
c.SubjectAccessReviewClient,
@ -56,5 +64,6 @@ func (c DelegatingAuthorizerConfig) New() (authorizer.Authorizer, error) {
*c.WebhookRetryBackoff,
authorizer.DecisionNoOpinion,
NewDelegatingAuthorizerMetrics(),
compiler,
)
}

View File

@ -65,6 +65,12 @@ type compiler struct {
envSet *environment.EnvSet
}
// NewDefaultCompiler returns a new Compiler following the default compatibility version.
// Note: the compiler construction depends on feature gates and the compatibility version to be initialized.
func NewDefaultCompiler() Compiler {
return NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
}
// NewCompiler returns a new Compiler.
func NewCompiler(env *environment.EnvSet) Compiler {
return &compiler{

View File

@ -25,7 +25,6 @@ import (
v1 "k8s.io/api/authorization/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/environment"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
@ -100,7 +99,10 @@ func TestCompileCELExpression(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, tc.authorizeWithSelectorsEnabled)
compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
// create new compiler because it depends on the feature gate
compiler := NewDefaultCompiler()
_, err := compiler.CompileCELExpression(&SubjectAccessReviewMatchCondition{
Expression: tc.expression,
})

View File

@ -86,6 +86,12 @@ type Options struct {
// Optional http.Client used to make all requests to the remote issuer. Mutually exclusive with CAContentProvider.
Client *http.Client
// Optional CEL compiler used to compile the CEL expressions. This is useful to use a shared instance
// of the compiler as these compilers holding a CEL environment are expensive to create. If not provided,
// a default compiler will be created.
// Note: the compiler construction depends on feature gates and the compatibility version to be initialized.
Compiler authenticationcel.Compiler
// SupportedSigningAlgs sets the accepted set of JOSE signing algorithms that
// can be used by the provider to sign tokens.
//
@ -245,7 +251,11 @@ type AuthenticatorTokenWithHealthCheck interface {
// Thus, once the lifecycleCtx is canceled, the authenticator must not be used.
// A caller may check if the authenticator is healthy by calling the HealthCheck method.
func New(lifecycleCtx context.Context, opts Options) (AuthenticatorTokenWithHealthCheck, error) {
celMapper, fieldErr := apiservervalidation.CompileAndValidateJWTAuthenticator(opts.JWTAuthenticator, opts.DisallowedIssuers)
compiler := opts.Compiler
if compiler == nil {
compiler = authenticationcel.NewDefaultCompiler()
}
celMapper, fieldErr := apiservervalidation.CompileAndValidateJWTAuthenticator(compiler, opts.JWTAuthenticator, opts.DisallowedIssuers)
if err := fieldErr.ToAggregate(); err != nil {
return nil, err
}

View File

@ -122,7 +122,7 @@ func TestAuthorizerMetrics(t *testing.T) {
defer server.Close()
fakeAuthzMetrics := &fakeAuthorizerMetrics{}
wh, err := newV1Authorizer(server.URL, scenario.clientCert, scenario.clientKey, scenario.clientCA, 0, fakeAuthzMetrics, []apiserver.WebhookMatchCondition{}, "")
wh, err := newV1Authorizer(server.URL, scenario.clientCert, scenario.clientKey, scenario.clientCA, 0, fakeAuthzMetrics, cel.NewDefaultCompiler(), []apiserver.WebhookMatchCondition{}, "")
if err != nil {
t.Error("failed to create client")
return

View File

@ -82,8 +82,8 @@ type WebhookAuthorizer struct {
}
// NewFromInterface creates a WebhookAuthorizer using the given subjectAccessReview client
func NewFromInterface(subjectAccessReview authorizationv1client.AuthorizationV1Interface, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, decisionOnError authorizer.Decision, metrics metrics.AuthorizerMetrics) (*WebhookAuthorizer, error) {
return newWithBackoff(&subjectAccessReviewV1Client{subjectAccessReview.RESTClient()}, authorizedTTL, unauthorizedTTL, retryBackoff, decisionOnError, nil, metrics, "")
func NewFromInterface(subjectAccessReview authorizationv1client.AuthorizationV1Interface, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, decisionOnError authorizer.Decision, metrics metrics.AuthorizerMetrics, compiler authorizationcel.Compiler) (*WebhookAuthorizer, error) {
return newWithBackoff(&subjectAccessReviewV1Client{subjectAccessReview.RESTClient()}, authorizedTTL, unauthorizedTTL, retryBackoff, decisionOnError, nil, metrics, compiler, "")
}
// New creates a new WebhookAuthorizer from the provided kubeconfig file.
@ -105,18 +105,18 @@ func NewFromInterface(subjectAccessReview authorizationv1client.AuthorizationV1I
//
// For additional HTTP configuration, refer to the kubeconfig documentation
// https://kubernetes.io/docs/user-guide/kubeconfig-file/.
func New(config *rest.Config, version string, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, decisionOnError authorizer.Decision, matchConditions []apiserver.WebhookMatchCondition, name string, metrics metrics.AuthorizerMetrics) (*WebhookAuthorizer, error) {
func New(config *rest.Config, version string, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, decisionOnError authorizer.Decision, matchConditions []apiserver.WebhookMatchCondition, name string, metrics metrics.AuthorizerMetrics, compiler authorizationcel.Compiler) (*WebhookAuthorizer, error) {
subjectAccessReview, err := subjectAccessReviewInterfaceFromConfig(config, version, retryBackoff)
if err != nil {
return nil, err
}
return newWithBackoff(subjectAccessReview, authorizedTTL, unauthorizedTTL, retryBackoff, decisionOnError, matchConditions, metrics, name)
return newWithBackoff(subjectAccessReview, authorizedTTL, unauthorizedTTL, retryBackoff, decisionOnError, matchConditions, metrics, compiler, name)
}
// newWithBackoff allows tests to skip the sleep.
func newWithBackoff(subjectAccessReview subjectAccessReviewer, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, decisionOnError authorizer.Decision, matchConditions []apiserver.WebhookMatchCondition, am metrics.AuthorizerMetrics, name string) (*WebhookAuthorizer, error) {
func newWithBackoff(subjectAccessReview subjectAccessReviewer, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, decisionOnError authorizer.Decision, matchConditions []apiserver.WebhookMatchCondition, am metrics.AuthorizerMetrics, compiler authorizationcel.Compiler, name string) (*WebhookAuthorizer, error) {
// compile all expressions once in validation and save the results to be used for eval later
cm, fieldErr := apiservervalidation.ValidateAndCompileMatchConditions(matchConditions)
cm, fieldErr := apiservervalidation.ValidateAndCompileMatchConditions(compiler, matchConditions)
if err := fieldErr.ToAggregate(); err != nil {
return nil, err
}

View File

@ -34,8 +34,6 @@ import (
"text/template"
"time"
utiltesting "k8s.io/client-go/util/testing"
"github.com/google/go-cmp/cmp"
authorizationv1 "k8s.io/api/authorization/v1"
@ -47,12 +45,13 @@ import (
"k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
celmetrics "k8s.io/apiserver/pkg/authorization/cel"
authorizationcel "k8s.io/apiserver/pkg/authorization/cel"
"k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/apiserver/plugin/pkg/authorizer/webhook/metrics"
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
utiltesting "k8s.io/client-go/util/testing"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/component-base/metrics/legacyregistry"
"k8s.io/component-base/metrics/testutil"
@ -217,7 +216,7 @@ current-context: default
if err != nil {
return fmt.Errorf("error building sar client: %v", err)
}
_, err = newWithBackoff(sarClient, 0, 0, testRetryBackoff, authorizer.DecisionNoOpinion, []apiserver.WebhookMatchCondition{}, noopAuthorizerMetrics(), "")
_, err = newWithBackoff(sarClient, 0, 0, testRetryBackoff, authorizer.DecisionNoOpinion, []apiserver.WebhookMatchCondition{}, noopAuthorizerMetrics(), authorizationcel.NewDefaultCompiler(), "")
return err
}()
if err != nil && !tt.wantErr {
@ -337,7 +336,7 @@ func (m *mockV1Service) HTTPStatusCode() int { return m.statusCode }
// newV1Authorizer creates a temporary kubeconfig file from the provided arguments and attempts to load
// a new WebhookAuthorizer from it.
func newV1Authorizer(callbackURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, metrics metrics.AuthorizerMetrics, expressions []apiserver.WebhookMatchCondition, authzName string) (*WebhookAuthorizer, error) {
func newV1Authorizer(callbackURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, metrics metrics.AuthorizerMetrics, compiler authorizationcel.Compiler, expressions []apiserver.WebhookMatchCondition, authzName string) (*WebhookAuthorizer, error) {
tempfile, err := ioutil.TempFile("", "")
if err != nil {
return nil, err
@ -367,7 +366,7 @@ func newV1Authorizer(callbackURL string, clientCert, clientKey, ca []byte, cache
if err != nil {
return nil, fmt.Errorf("error building sar client: %v", err)
}
return newWithBackoff(sarClient, cacheTime, cacheTime, testRetryBackoff, authorizer.DecisionNoOpinion, expressions, metrics, authzName)
return newWithBackoff(sarClient, cacheTime, cacheTime, testRetryBackoff, authorizer.DecisionNoOpinion, expressions, metrics, compiler, authzName)
}
func TestV1TLSConfig(t *testing.T) {
@ -426,7 +425,7 @@ func TestV1TLSConfig(t *testing.T) {
}
defer server.Close()
wh, err := newV1Authorizer(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0, noopAuthorizerMetrics(), []apiserver.WebhookMatchCondition{}, "")
wh, err := newV1Authorizer(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0, noopAuthorizerMetrics(), authorizationcel.NewDefaultCompiler(), []apiserver.WebhookMatchCondition{}, "")
if err != nil {
t.Errorf("%s: failed to create client: %v", tt.test, err)
return
@ -491,7 +490,7 @@ func TestV1Webhook(t *testing.T) {
}
defer s.Close()
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), []apiserver.WebhookMatchCondition{}, "")
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), authorizationcel.NewDefaultCompiler(), []apiserver.WebhookMatchCondition{}, "")
if err != nil {
t.Fatal(err)
}
@ -598,7 +597,7 @@ func TestV1WebhookCache(t *testing.T) {
},
}
// Create an authorizer that caches successful responses "forever" (100 days).
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 2400*time.Hour, noopAuthorizerMetrics(), expressions, "")
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 2400*time.Hour, noopAuthorizerMetrics(), authorizationcel.NewDefaultCompiler(), expressions, "")
if err != nil {
t.Fatal(err)
}
@ -693,7 +692,6 @@ func TestV1WebhookCache(t *testing.T) {
// TestStructuredAuthzConfigFeatureEnablement verifies cel expressions can only be used when feature is enabled
func TestStructuredAuthzConfigFeatureEnablement(t *testing.T) {
service := new(mockV1Service)
service.statusCode = 200
service.Allow()
@ -804,7 +802,11 @@ func TestStructuredAuthzConfigFeatureEnablement(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, test.featureEnabled)
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AuthorizeWithSelectors, test.selectorEnabled)
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), test.expressions, "")
// create new compiler because it depends on the feature gate
compiler := authorizationcel.NewDefaultCompiler()
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), compiler, test.expressions, "")
if test.expectedCompileErr && err == nil {
t.Fatalf("%d: Expected compile error", i)
} else if !test.expectedCompileErr && err != nil {
@ -910,13 +912,13 @@ func TestWebhookMetrics(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
celmetrics.ResetMetricsForTest()
defer celmetrics.ResetMetricsForTest()
wh1, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, celAuthorizerMetrics(), tt.expressions1, "wh1.example.com")
authorizationcel.ResetMetricsForTest()
defer authorizationcel.ResetMetricsForTest()
wh1, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, celAuthorizerMetrics(), authorizationcel.NewDefaultCompiler(), tt.expressions1, "wh1.example.com")
if err != nil {
t.Fatal(err)
}
wh2, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, celAuthorizerMetrics(), tt.expressions2, "wh2.example.com")
wh2, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, celAuthorizerMetrics(), authorizationcel.NewDefaultCompiler(), tt.expressions2, "wh2.example.com")
if err != nil {
t.Fatal(err)
}
@ -1092,7 +1094,7 @@ func benchmarkNewWebhookAuthorizer(b *testing.B, expressions []apiserver.Webhook
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Create an authorizer with or without expressions to compile
_, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), expressions, "")
_, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), authorizationcel.NewDefaultCompiler(), expressions, "")
if err != nil {
b.Fatal(err)
}
@ -1122,7 +1124,7 @@ func benchmarkWebhookAuthorize(b *testing.B, expressions []apiserver.WebhookMatc
defer s.Close()
featuregatetesting.SetFeatureGateDuringTest(b, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, featureEnabled)
// Create an authorizer with or without expressions to compile
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), expressions, "")
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), authorizationcel.NewDefaultCompiler(), expressions, "")
if err != nil {
b.Fatal(err)
}
@ -1409,7 +1411,7 @@ func TestV1WebhookMatchConditions(t *testing.T) {
for i, test := range tests {
t.Run(test.name, func(t *testing.T) {
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), test.expressions, "")
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), authorizationcel.NewDefaultCompiler(), test.expressions, "")
if len(test.expectedCompileErr) > 0 && err == nil {
t.Fatalf("%d: Expected compile error", i)
} else if len(test.expectedCompileErr) == 0 && err != nil {
@ -1448,12 +1450,12 @@ func noopAuthorizerMetrics() metrics.AuthorizerMetrics {
func celAuthorizerMetrics() metrics.AuthorizerMetrics {
return celAuthorizerMetricsType{
MatcherMetrics: celmetrics.NewMatcherMetrics(),
MatcherMetrics: authorizationcel.NewMatcherMetrics(),
}
}
type celAuthorizerMetricsType struct {
metrics.NoopRequestMetrics
metrics.NoopWebhookMetrics
celmetrics.MatcherMetrics
authorizationcel.MatcherMetrics
}

View File

@ -41,6 +41,7 @@ import (
authzconfig "k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
authorizationcel "k8s.io/apiserver/pkg/authorization/cel"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
)
@ -197,7 +198,7 @@ current-context: default
if err != nil {
return fmt.Errorf("error building sar client: %v", err)
}
_, err = newWithBackoff(sarClient, 0, 0, testRetryBackoff, authorizer.DecisionNoOpinion, []authzconfig.WebhookMatchCondition{}, noopAuthorizerMetrics(), "")
_, err = newWithBackoff(sarClient, 0, 0, testRetryBackoff, authorizer.DecisionNoOpinion, []authzconfig.WebhookMatchCondition{}, noopAuthorizerMetrics(), authorizationcel.NewDefaultCompiler(), "")
return err
}()
if err != nil && !tt.wantErr {
@ -340,7 +341,7 @@ func newV1beta1Authorizer(callbackURL string, clientCert, clientKey, ca []byte,
if err != nil {
return nil, fmt.Errorf("error building sar client: %v", err)
}
return newWithBackoff(sarClient, cacheTime, cacheTime, testRetryBackoff, authorizer.DecisionNoOpinion, []authzconfig.WebhookMatchCondition{}, noopAuthorizerMetrics(), "")
return newWithBackoff(sarClient, cacheTime, cacheTime, testRetryBackoff, authorizer.DecisionNoOpinion, []authzconfig.WebhookMatchCondition{}, noopAuthorizerMetrics(), authorizationcel.NewDefaultCompiler(), "")
}
func TestV1beta1TLSConfig(t *testing.T) {