add authz webhook matchcondition metrics

Signed-off-by: Rita Zhang <rita.z.zhang@gmail.com>
Signed-off-by: Jordan Liggitt <liggitt@google.com>
Co-authored-by: Jordan Liggitt <liggitt@google.com>
This commit is contained in:
Rita Zhang 2024-02-29 20:55:32 -08:00
parent 8b8d133770
commit e76fce7566
No known key found for this signature in database
GPG Key ID: 3ADE11B31515DF8C
12 changed files with 520 additions and 89 deletions

View File

@ -32,11 +32,13 @@ import (
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/authorization/authorizerfactory" "k8s.io/apiserver/pkg/authorization/authorizerfactory"
"k8s.io/apiserver/pkg/authorization/cel"
authorizationmetrics "k8s.io/apiserver/pkg/authorization/metrics" authorizationmetrics "k8s.io/apiserver/pkg/authorization/metrics"
"k8s.io/apiserver/pkg/authorization/union" "k8s.io/apiserver/pkg/authorization/union"
"k8s.io/apiserver/pkg/server/options/authorizationconfig/metrics" "k8s.io/apiserver/pkg/server/options/authorizationconfig/metrics"
webhookutil "k8s.io/apiserver/pkg/util/webhook" webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/apiserver/plugin/pkg/authorizer/webhook" "k8s.io/apiserver/plugin/pkg/authorizer/webhook"
webhookmetrics "k8s.io/apiserver/plugin/pkg/authorizer/webhook/metrics"
"k8s.io/klog/v2" "k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/auth/authorizer/abac" "k8s.io/kubernetes/pkg/auth/authorizer/abac"
"k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes" "k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes"
@ -142,6 +144,8 @@ func (r *reloadableAuthorizerResolver) newForConfig(authzConfig *authzconfig.Aut
*r.initialConfig.WebhookRetryBackoff, *r.initialConfig.WebhookRetryBackoff,
decisionOnError, decisionOnError,
configuredAuthorizer.Webhook.MatchConditions, configuredAuthorizer.Webhook.MatchConditions,
configuredAuthorizer.Name,
kubeapiserverWebhookMetrics{MatcherMetrics: cel.NewMatcherMetrics()},
) )
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -162,6 +166,13 @@ func (r *reloadableAuthorizerResolver) newForConfig(authzConfig *authzconfig.Aut
return union.New(authorizers...), union.NewRuleResolvers(ruleResolvers...), nil return union.New(authorizers...), union.NewRuleResolvers(ruleResolvers...), nil
} }
type kubeapiserverWebhookMetrics struct {
// kube-apiserver doesn't report request metrics
webhookmetrics.NoopRequestMetrics
// kube-apiserver does report matchCondition metrics
cel.MatcherMetrics
}
// runReload starts checking the config file for changes and reloads the authorizer when it changes. // runReload starts checking the config file for changes and reloads the authorizer when it changes.
// Blocks until ctx is complete. // Blocks until ctx is complete.
func (r *reloadableAuthorizerResolver) runReload(ctx context.Context) { func (r *reloadableAuthorizerResolver) runReload(ctx context.Context) {

View File

@ -26,7 +26,7 @@ import (
authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1" authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1"
) )
// DelegatingAuthorizerConfig is the minimal configuration needed to create an authenticator // DelegatingAuthorizerConfig is the minimal configuration needed to create an authorizer
// built to delegate authorization to a kube API server // built to delegate authorization to a kube API server
type DelegatingAuthorizerConfig struct { type DelegatingAuthorizerConfig struct {
SubjectAccessReviewClient authorizationclient.AuthorizationV1Interface SubjectAccessReviewClient authorizationclient.AuthorizationV1Interface
@ -55,9 +55,6 @@ func (c DelegatingAuthorizerConfig) New() (authorizer.Authorizer, error) {
c.DenyCacheTTL, c.DenyCacheTTL,
*c.WebhookRetryBackoff, *c.WebhookRetryBackoff,
authorizer.DecisionNoOpinion, authorizer.DecisionNoOpinion,
webhook.AuthorizerMetrics{ NewDelegatingAuthorizerMetrics(),
RecordRequestTotal: RecordRequestTotal,
RecordRequestLatency: RecordRequestLatency,
},
) )
} }

View File

@ -18,18 +18,22 @@ package authorizerfactory
import ( import (
"context" "context"
"sync"
celmetrics "k8s.io/apiserver/pkg/authorization/cel"
webhookmetrics "k8s.io/apiserver/plugin/pkg/authorizer/webhook/metrics"
compbasemetrics "k8s.io/component-base/metrics" compbasemetrics "k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry" "k8s.io/component-base/metrics/legacyregistry"
) )
type registerables []compbasemetrics.Registerable var registerMetrics sync.Once
// init registers all metrics // RegisterMetrics registers authorizer metrics.
func init() { func RegisterMetrics() {
for _, metric := range metrics { registerMetrics.Do(func() {
legacyregistry.MustRegister(metric) legacyregistry.MustRegister(requestTotal)
} legacyregistry.MustRegister(requestLatency)
})
} }
var ( var (
@ -51,19 +55,26 @@ var (
}, },
[]string{"code"}, []string{"code"},
) )
metrics = registerables{
requestTotal,
requestLatency,
}
) )
var _ = webhookmetrics.AuthorizerMetrics(delegatingAuthorizerMetrics{})
type delegatingAuthorizerMetrics struct {
// no-op for matchCondition metrics for now, delegating authorization doesn't configure match conditions
celmetrics.NoopMatcherMetrics
}
func NewDelegatingAuthorizerMetrics() delegatingAuthorizerMetrics {
RegisterMetrics()
return delegatingAuthorizerMetrics{}
}
// RecordRequestTotal increments the total number of requests for the delegated authorization. // RecordRequestTotal increments the total number of requests for the delegated authorization.
func RecordRequestTotal(ctx context.Context, code string) { func (delegatingAuthorizerMetrics) RecordRequestTotal(ctx context.Context, code string) {
requestTotal.WithContext(ctx).WithLabelValues(code).Add(1) requestTotal.WithContext(ctx).WithLabelValues(code).Add(1)
} }
// RecordRequestLatency measures request latency in seconds for the delegated authorization. Broken down by status code. // RecordRequestLatency measures request latency in seconds for the delegated authorization. Broken down by status code.
func RecordRequestLatency(ctx context.Context, code string, latency float64) { func (delegatingAuthorizerMetrics) RecordRequestLatency(ctx context.Context, code string, latency float64) {
requestLatency.WithContext(ctx).WithLabelValues(code).Observe(latency) requestLatency.WithContext(ctx).WithLabelValues(code).Observe(latency)
} }

View File

@ -19,6 +19,7 @@ package cel
import ( import (
"context" "context"
"fmt" "fmt"
"time"
celgo "github.com/google/cel-go/cel" celgo "github.com/google/cel-go/cel"
@ -28,11 +29,29 @@ import (
type CELMatcher struct { type CELMatcher struct {
CompilationResults []CompilationResult CompilationResults []CompilationResult
// These are optional fields which can be populated if metrics reporting is desired
Metrics MatcherMetrics
AuthorizerType string
AuthorizerName string
} }
// eval evaluates the given SubjectAccessReview against all cel matchCondition expression // eval evaluates the given SubjectAccessReview against all cel matchCondition expression
func (c *CELMatcher) Eval(ctx context.Context, r *authorizationv1.SubjectAccessReview) (bool, error) { func (c *CELMatcher) Eval(ctx context.Context, r *authorizationv1.SubjectAccessReview) (bool, error) {
var evalErrors []error var evalErrors []error
metrics := c.Metrics
if metrics == nil {
metrics = NoopMatcherMetrics{}
}
start := time.Now()
defer func() {
metrics.RecordAuthorizationMatchConditionEvaluation(ctx, c.AuthorizerType, c.AuthorizerName, time.Since(start))
if len(evalErrors) > 0 {
metrics.RecordAuthorizationMatchConditionEvaluationFailure(ctx, c.AuthorizerType, c.AuthorizerName)
}
}()
va := map[string]interface{}{ va := map[string]interface{}{
"request": convertObjectToUnstructured(&r.Spec), "request": convertObjectToUnstructured(&r.Spec),
} }
@ -54,6 +73,7 @@ func (c *CELMatcher) Eval(ctx context.Context, r *authorizationv1.SubjectAccessR
// If at least one matchCondition successfully evaluates to FALSE, // If at least one matchCondition successfully evaluates to FALSE,
// return early // return early
if !match { if !match {
metrics.RecordAuthorizationMatchConditionExclusion(ctx, c.AuthorizerType, c.AuthorizerName)
return false, nil return false, nil
} }
} }

View File

@ -0,0 +1,120 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
import (
"context"
"sync"
"time"
"k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry"
)
// MatcherMetrics defines methods for reporting matchCondition metrics
type MatcherMetrics interface {
// RecordAuthorizationMatchConditionEvaluation records the total time taken to evaluate matchConditions for an Authorize() call to the given authorizer
RecordAuthorizationMatchConditionEvaluation(ctx context.Context, authorizerType, authorizerName string, elapsed time.Duration)
// RecordAuthorizationMatchConditionEvaluationFailure increments if any evaluation error was encountered evaluating matchConditions for an Authorize() call to the given authorizer
RecordAuthorizationMatchConditionEvaluationFailure(ctx context.Context, authorizerType, authorizerName string)
// RecordAuthorizationMatchConditionExclusion records increments when at least one matchCondition evaluates to false and excludes an Authorize() call to the given authorizer
RecordAuthorizationMatchConditionExclusion(ctx context.Context, authorizerType, authorizerName string)
}
type NoopMatcherMetrics struct{}
func (NoopMatcherMetrics) RecordAuthorizationMatchConditionEvaluation(ctx context.Context, authorizerType, authorizerName string, elapsed time.Duration) {
}
func (NoopMatcherMetrics) RecordAuthorizationMatchConditionEvaluationFailure(ctx context.Context, authorizerType, authorizerName string) {
}
func (NoopMatcherMetrics) RecordAuthorizationMatchConditionExclusion(ctx context.Context, authorizerType, authorizerName string) {
}
type matcherMetrics struct{}
func NewMatcherMetrics() MatcherMetrics {
RegisterMetrics()
return matcherMetrics{}
}
const (
namespace = "apiserver"
subsystem = "authorization"
)
var (
authorizationMatchConditionEvaluationErrorsTotal = metrics.NewCounterVec(
&metrics.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "match_condition_evaluation_errors_total",
Help: "Total number of errors when an authorization webhook encounters a match condition error split by authorizer type and name.",
StabilityLevel: metrics.ALPHA,
},
[]string{"type", "name"},
)
authorizationMatchConditionExclusionsTotal = metrics.NewCounterVec(
&metrics.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "match_condition_exclusions_total",
Help: "Total number of exclusions when an authorization webhook is skipped because match conditions exclude it.",
StabilityLevel: metrics.ALPHA,
},
[]string{"type", "name"},
)
authorizationMatchConditionEvaluationSeconds = metrics.NewHistogramVec(
&metrics.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "match_condition_evaluation_seconds",
Help: "Authorization match condition evaluation time in seconds, split by authorizer type and name.",
Buckets: []float64{0.001, 0.005, 0.01, 0.025, 0.1, 0.2, 0.25},
StabilityLevel: metrics.ALPHA,
},
[]string{"type", "name"},
)
)
var registerMetrics sync.Once
func RegisterMetrics() {
registerMetrics.Do(func() {
legacyregistry.MustRegister(authorizationMatchConditionEvaluationErrorsTotal)
legacyregistry.MustRegister(authorizationMatchConditionExclusionsTotal)
legacyregistry.MustRegister(authorizationMatchConditionEvaluationSeconds)
})
}
func ResetMetricsForTest() {
authorizationMatchConditionEvaluationErrorsTotal.Reset()
authorizationMatchConditionExclusionsTotal.Reset()
authorizationMatchConditionEvaluationSeconds.Reset()
}
func (matcherMetrics) RecordAuthorizationMatchConditionEvaluationFailure(ctx context.Context, authorizerType, authorizerName string) {
authorizationMatchConditionEvaluationErrorsTotal.WithContext(ctx).WithLabelValues(authorizerType, authorizerName).Inc()
}
func (matcherMetrics) RecordAuthorizationMatchConditionExclusion(ctx context.Context, authorizerType, authorizerName string) {
authorizationMatchConditionExclusionsTotal.WithContext(ctx).WithLabelValues(authorizerType, authorizerName).Inc()
}
func (matcherMetrics) RecordAuthorizationMatchConditionEvaluation(ctx context.Context, authorizerType, authorizerName string, elapsed time.Duration) {
elapsedSeconds := elapsed.Seconds()
authorizationMatchConditionEvaluationSeconds.WithContext(ctx).WithLabelValues(authorizerType, authorizerName).Observe(elapsedSeconds)
}

View File

@ -0,0 +1,81 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
import (
"context"
"strings"
"testing"
"time"
"k8s.io/component-base/metrics/legacyregistry"
"k8s.io/component-base/metrics/testutil"
)
func TestRecordAuthorizationMatchConditionEvaluationFailure(t *testing.T) {
testCases := []struct {
desc string
metrics []string
name string
authztype string
want string
}{
{
desc: "evaluation failure total",
metrics: []string{
"apiserver_authorization_match_condition_evaluation_errors_total",
"apiserver_authorization_match_condition_exclusions_total",
"apiserver_authorization_match_condition_evaluation_seconds",
},
name: "wh1.example.com",
authztype: "Webhook",
want: `
# HELP apiserver_authorization_match_condition_evaluation_errors_total [ALPHA] Total number of errors when an authorization webhook encounters a match condition error split by authorizer type and name.
# TYPE apiserver_authorization_match_condition_evaluation_errors_total counter
apiserver_authorization_match_condition_evaluation_errors_total{name="wh1.example.com",type="Webhook"} 1
# HELP apiserver_authorization_match_condition_evaluation_seconds [ALPHA] Authorization match condition evaluation time in seconds, split by authorizer type and name.
# TYPE apiserver_authorization_match_condition_evaluation_seconds histogram
apiserver_authorization_match_condition_evaluation_seconds_bucket{name="wh1.example.com",type="Webhook",le="0.001"} 0
apiserver_authorization_match_condition_evaluation_seconds_bucket{name="wh1.example.com",type="Webhook",le="0.005"} 0
apiserver_authorization_match_condition_evaluation_seconds_bucket{name="wh1.example.com",type="Webhook",le="0.01"} 0
apiserver_authorization_match_condition_evaluation_seconds_bucket{name="wh1.example.com",type="Webhook",le="0.025"} 0
apiserver_authorization_match_condition_evaluation_seconds_bucket{name="wh1.example.com",type="Webhook",le="0.1"} 0
apiserver_authorization_match_condition_evaluation_seconds_bucket{name="wh1.example.com",type="Webhook",le="0.2"} 0
apiserver_authorization_match_condition_evaluation_seconds_bucket{name="wh1.example.com",type="Webhook",le="0.25"} 0
apiserver_authorization_match_condition_evaluation_seconds_bucket{name="wh1.example.com",type="Webhook",le="+Inf"} 1
apiserver_authorization_match_condition_evaluation_seconds_sum{name="wh1.example.com",type="Webhook"} 1
apiserver_authorization_match_condition_evaluation_seconds_count{name="wh1.example.com",type="Webhook"} 1
# HELP apiserver_authorization_match_condition_exclusions_total [ALPHA] Total number of exclusions when an authorization webhook is skipped because match conditions exclude it.
# TYPE apiserver_authorization_match_condition_exclusions_total counter
apiserver_authorization_match_condition_exclusions_total{name="wh1.example.com",type="Webhook"} 1
`,
},
}
for _, tt := range testCases {
t.Run(tt.desc, func(t *testing.T) {
ResetMetricsForTest()
m := NewMatcherMetrics()
m.RecordAuthorizationMatchConditionEvaluationFailure(context.Background(), tt.authztype, tt.name)
m.RecordAuthorizationMatchConditionExclusion(context.Background(), tt.authztype, tt.name)
m.RecordAuthorizationMatchConditionEvaluation(context.Background(), tt.authztype, tt.name, time.Duration(1*time.Second))
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tt.want), tt.metrics...); err != nil {
t.Fatal(err)
}
})
}
}

View File

@ -14,22 +14,36 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package webhook package metrics
import ( import (
"context" "context"
"k8s.io/apiserver/pkg/authorization/cel"
) )
// AuthorizerMetrics specifies a set of methods that are used to register various metrics for the webhook authorizer // AuthorizerMetrics specifies a set of methods that are used to register various metrics for the webhook authorizer
type AuthorizerMetrics struct { type AuthorizerMetrics interface {
// RecordRequestTotal increments the total number of requests for the webhook authorizer // Request total and latency metrics
RecordRequestTotal func(ctx context.Context, code string) RequestMetrics
// match condition metrics
// RecordRequestLatency measures request latency in seconds for webhooks. Broken down by status code. cel.MatcherMetrics
RecordRequestLatency func(ctx context.Context, code string, latency float64)
} }
type noopMetrics struct{} type NoopAuthorizerMetrics struct {
NoopRequestMetrics
cel.NoopMatcherMetrics
}
func (noopMetrics) RecordRequestTotal(context.Context, string) {} type RequestMetrics interface {
func (noopMetrics) RecordRequestLatency(context.Context, string, float64) {} // RecordRequestTotal increments the total number of requests for the webhook authorizer
RecordRequestTotal(ctx context.Context, code string)
// RecordRequestLatency measures request latency in seconds for webhooks. Broken down by status code.
RecordRequestLatency(ctx context.Context, code string, latency float64)
}
type NoopRequestMetrics struct{}
func (NoopRequestMetrics) RecordRequestTotal(context.Context, string) {}
func (NoopRequestMetrics) RecordRequestLatency(context.Context, string, float64) {}

View File

@ -23,6 +23,7 @@ import (
"k8s.io/apiserver/pkg/apis/apiserver" "k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/authorization/cel"
) )
func TestAuthorizerMetrics(t *testing.T) { func TestAuthorizerMetrics(t *testing.T) {
@ -76,11 +77,7 @@ func TestAuthorizerMetrics(t *testing.T) {
defer server.Close() defer server.Close()
fakeAuthzMetrics := &fakeAuthorizerMetrics{} fakeAuthzMetrics := &fakeAuthorizerMetrics{}
authzMetrics := AuthorizerMetrics{ wh, err := newV1Authorizer(server.URL, scenario.clientCert, scenario.clientKey, scenario.clientCA, 0, fakeAuthzMetrics, []apiserver.WebhookMatchCondition{}, "")
RecordRequestTotal: fakeAuthzMetrics.RequestTotal,
RecordRequestLatency: fakeAuthzMetrics.RequestLatency,
}
wh, err := newV1Authorizer(server.URL, scenario.clientCert, scenario.clientKey, scenario.clientCA, 0, authzMetrics, []apiserver.WebhookMatchCondition{})
if err != nil { if err != nil {
t.Error("failed to create client") t.Error("failed to create client")
return return
@ -110,13 +107,15 @@ type fakeAuthorizerMetrics struct {
latency float64 latency float64
latencyCode string latencyCode string
cel.NoopMatcherMetrics
} }
func (f *fakeAuthorizerMetrics) RequestTotal(_ context.Context, code string) { func (f *fakeAuthorizerMetrics) RecordRequestTotal(_ context.Context, code string) {
f.totalCode = code f.totalCode = code
} }
func (f *fakeAuthorizerMetrics) RequestLatency(_ context.Context, code string, latency float64) { func (f *fakeAuthorizerMetrics) RecordRequestLatency(_ context.Context, code string, latency float64) {
f.latency = latency f.latency = latency
f.latencyCode = code f.latencyCode = code
} }

View File

@ -39,6 +39,7 @@ import (
"k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/apiserver/pkg/util/webhook" "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/apiserver/plugin/pkg/authorizer/webhook/metrics"
"k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/kubernetes/scheme"
authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1" authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
@ -70,13 +71,14 @@ type WebhookAuthorizer struct {
unauthorizedTTL time.Duration unauthorizedTTL time.Duration
retryBackoff wait.Backoff retryBackoff wait.Backoff
decisionOnError authorizer.Decision decisionOnError authorizer.Decision
metrics AuthorizerMetrics metrics metrics.AuthorizerMetrics
celMatcher *authorizationcel.CELMatcher celMatcher *authorizationcel.CELMatcher
name string
} }
// NewFromInterface creates a WebhookAuthorizer using the given subjectAccessReview client // 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 AuthorizerMetrics) (*WebhookAuthorizer, error) { 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) return newWithBackoff(&subjectAccessReviewV1Client{subjectAccessReview.RESTClient()}, authorizedTTL, unauthorizedTTL, retryBackoff, decisionOnError, nil, metrics, "")
} }
// New creates a new WebhookAuthorizer from the provided kubeconfig file. // New creates a new WebhookAuthorizer from the provided kubeconfig file.
@ -98,24 +100,26 @@ func NewFromInterface(subjectAccessReview authorizationv1client.AuthorizationV1I
// //
// For additional HTTP configuration, refer to the kubeconfig documentation // For additional HTTP configuration, refer to the kubeconfig documentation
// https://kubernetes.io/docs/user-guide/kubeconfig-file/. // 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) (*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) (*WebhookAuthorizer, error) {
subjectAccessReview, err := subjectAccessReviewInterfaceFromConfig(config, version, retryBackoff) subjectAccessReview, err := subjectAccessReviewInterfaceFromConfig(config, version, retryBackoff)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return newWithBackoff(subjectAccessReview, authorizedTTL, unauthorizedTTL, retryBackoff, decisionOnError, matchConditions, AuthorizerMetrics{ return newWithBackoff(subjectAccessReview, authorizedTTL, unauthorizedTTL, retryBackoff, decisionOnError, matchConditions, metrics, name)
RecordRequestTotal: noopMetrics{}.RecordRequestTotal,
RecordRequestLatency: noopMetrics{}.RecordRequestLatency,
})
} }
// newWithBackoff allows tests to skip the sleep. // newWithBackoff allows tests to skip the sleep.
func newWithBackoff(subjectAccessReview subjectAccessReviewer, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, decisionOnError authorizer.Decision, matchConditions []apiserver.WebhookMatchCondition, metrics AuthorizerMetrics) (*WebhookAuthorizer, error) { func newWithBackoff(subjectAccessReview subjectAccessReviewer, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, decisionOnError authorizer.Decision, matchConditions []apiserver.WebhookMatchCondition, am metrics.AuthorizerMetrics, name string) (*WebhookAuthorizer, error) {
// compile all expressions once in validation and save the results to be used for eval later // 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(matchConditions)
if err := fieldErr.ToAggregate(); err != nil { if err := fieldErr.ToAggregate(); err != nil {
return nil, err return nil, err
} }
if cm != nil {
cm.AuthorizerType = "Webhook"
cm.AuthorizerName = name
cm.Metrics = am
}
return &WebhookAuthorizer{ return &WebhookAuthorizer{
subjectAccessReview: subjectAccessReview, subjectAccessReview: subjectAccessReview,
responseCache: cache.NewLRUExpireCache(8192), responseCache: cache.NewLRUExpireCache(8192),
@ -123,8 +127,9 @@ func newWithBackoff(subjectAccessReview subjectAccessReviewer, authorizedTTL, un
unauthorizedTTL: unauthorizedTTL, unauthorizedTTL: unauthorizedTTL,
retryBackoff: retryBackoff, retryBackoff: retryBackoff,
decisionOnError: decisionOnError, decisionOnError: decisionOnError,
metrics: metrics, metrics: am,
celMatcher: cm, celMatcher: cm,
name: name,
}, nil }, nil
} }

View File

@ -44,11 +44,15 @@ import (
"k8s.io/apiserver/pkg/apis/apiserver" "k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
celmetrics "k8s.io/apiserver/pkg/authorization/cel"
"k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
webhookutil "k8s.io/apiserver/pkg/util/webhook" webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/apiserver/plugin/pkg/authorizer/webhook/metrics"
v1 "k8s.io/client-go/tools/clientcmd/api/v1" v1 "k8s.io/client-go/tools/clientcmd/api/v1"
featuregatetesting "k8s.io/component-base/featuregate/testing" featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/component-base/metrics/legacyregistry"
"k8s.io/component-base/metrics/testutil"
) )
var testRetryBackoff = wait.Backoff{ var testRetryBackoff = wait.Backoff{
@ -210,7 +214,7 @@ current-context: default
if err != nil { if err != nil {
return fmt.Errorf("error building sar client: %v", err) 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(), "")
return err return err
}() }()
if err != nil && !tt.wantErr { if err != nil && !tt.wantErr {
@ -323,7 +327,7 @@ func (m *mockV1Service) HTTPStatusCode() int { return m.statusCode }
// newV1Authorizer creates a temporary kubeconfig file from the provided arguments and attempts to load // newV1Authorizer creates a temporary kubeconfig file from the provided arguments and attempts to load
// a new WebhookAuthorizer from it. // a new WebhookAuthorizer from it.
func newV1Authorizer(callbackURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, metrics AuthorizerMetrics, expressions []apiserver.WebhookMatchCondition) (*WebhookAuthorizer, error) { func newV1Authorizer(callbackURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, metrics metrics.AuthorizerMetrics, expressions []apiserver.WebhookMatchCondition, authzName string) (*WebhookAuthorizer, error) {
tempfile, err := ioutil.TempFile("", "") tempfile, err := ioutil.TempFile("", "")
if err != nil { if err != nil {
return nil, err return nil, err
@ -353,7 +357,7 @@ func newV1Authorizer(callbackURL string, clientCert, clientKey, ca []byte, cache
if err != nil { if err != nil {
return nil, fmt.Errorf("error building sar client: %v", err) return nil, fmt.Errorf("error building sar client: %v", err)
} }
return newWithBackoff(sarClient, cacheTime, cacheTime, testRetryBackoff, authorizer.DecisionNoOpinion, expressions, metrics) return newWithBackoff(sarClient, cacheTime, cacheTime, testRetryBackoff, authorizer.DecisionNoOpinion, expressions, metrics, authzName)
} }
func TestV1TLSConfig(t *testing.T) { func TestV1TLSConfig(t *testing.T) {
@ -412,7 +416,7 @@ func TestV1TLSConfig(t *testing.T) {
} }
defer server.Close() 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(), []apiserver.WebhookMatchCondition{}, "")
if err != nil { if err != nil {
t.Errorf("%s: failed to create client: %v", tt.test, err) t.Errorf("%s: failed to create client: %v", tt.test, err)
return return
@ -477,7 +481,7 @@ func TestV1Webhook(t *testing.T) {
} }
defer s.Close() 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(), []apiserver.WebhookMatchCondition{}, "")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -584,7 +588,7 @@ func TestV1WebhookCache(t *testing.T) {
}, },
} }
// Create an authorizer that caches successful responses "forever" (100 days). // 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(), expressions, "")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -760,7 +764,7 @@ func TestStructuredAuthzConfigFeatureEnablement(t *testing.T) {
for i, test := range tests { for i, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, test.featureEnabled)() defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, test.featureEnabled)()
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), test.expressions) wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), test.expressions, "")
if test.expectedCompileErr && err == nil { if test.expectedCompileErr && err == nil {
t.Fatalf("%d: Expected compile error", i) t.Fatalf("%d: Expected compile error", i)
} else if !test.expectedCompileErr && err != nil { } else if !test.expectedCompileErr && err != nil {
@ -782,6 +786,112 @@ func TestStructuredAuthzConfigFeatureEnablement(t *testing.T) {
} }
} }
func TestWebhookMetrics(t *testing.T) {
service := new(mockV1Service)
service.statusCode = 200
service.Allow()
s, err := NewV1TestServer(service, serverCert, serverKey, caCert)
if err != nil {
t.Fatal(err)
}
defer s.Close()
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, true)()
aliceAttr := authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "alice",
UID: "1",
},
}
testCases := []struct {
name string
attr authorizer.AttributesRecord
expressions1 []apiserver.WebhookMatchCondition
expressions2 []apiserver.WebhookMatchCondition
metrics []string
want string
}{
{
name: "should have one evaluation error from multiple failed match conditions",
attr: aliceAttr,
expressions1: []apiserver.WebhookMatchCondition{
{
Expression: "request.user == 'alice'",
},
{
Expression: "request.resourceAttributes.verb == 'get'",
},
{
Expression: "request.resourceAttributes.namespace == 'kittensandponies'",
},
},
expressions2: []apiserver.WebhookMatchCondition{
{
Expression: "request.user == 'alice'",
},
},
metrics: []string{
"apiserver_authorization_match_condition_evaluation_errors_total",
},
want: fmt.Sprintf(`
# HELP apiserver_authorization_match_condition_evaluation_errors_total [ALPHA] Total number of errors when an authorization webhook encounters a match condition error split by authorizer type and name.
# TYPE apiserver_authorization_match_condition_evaluation_errors_total counter
apiserver_authorization_match_condition_evaluation_errors_total{name="%s",type="%s"} 1
`, "wh1.example.com", "Webhook"),
},
{
name: "should have two webhook exclusions due to match condition",
attr: aliceAttr,
expressions1: []apiserver.WebhookMatchCondition{
{
Expression: "request.user == 'alice2'",
},
{
Expression: "request.uid == '1'",
},
},
expressions2: []apiserver.WebhookMatchCondition{
{
Expression: "request.user == 'alice1'",
},
},
metrics: []string{
"apiserver_authorization_match_condition_exclusions_total",
},
want: fmt.Sprintf(`
# HELP apiserver_authorization_match_condition_exclusions_total [ALPHA] Total number of exclusions when an authorization webhook is skipped because match conditions exclude it.
# TYPE apiserver_authorization_match_condition_exclusions_total counter
apiserver_authorization_match_condition_exclusions_total{name="%s",type="%s"} 1
apiserver_authorization_match_condition_exclusions_total{name="%s",type="%s"} 1
`, "wh1.example.com", "Webhook", "wh2.example.com", "Webhook"),
},
}
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")
if err != nil {
t.Fatal(err)
}
wh2, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, celAuthorizerMetrics(), tt.expressions2, "wh2.example.com")
if err != nil {
t.Fatal(err)
}
if err == nil {
_, _, _ = wh1.Authorize(context.Background(), tt.attr)
_, _, _ = wh2.Authorize(context.Background(), tt.attr)
}
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tt.want), tt.metrics...); err != nil {
t.Fatal(err)
}
})
}
}
func BenchmarkNoCELExpressionFeatureOff(b *testing.B) { func BenchmarkNoCELExpressionFeatureOff(b *testing.B) {
expressions := []apiserver.WebhookMatchCondition{} expressions := []apiserver.WebhookMatchCondition{}
b.Run("compile", func(b *testing.B) { b.Run("compile", func(b *testing.B) {
@ -942,7 +1052,7 @@ func benchmarkNewWebhookAuthorizer(b *testing.B, expressions []apiserver.Webhook
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
// Create an authorizer with or without expressions to compile // 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(), expressions, "")
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
@ -972,7 +1082,7 @@ func benchmarkWebhookAuthorize(b *testing.B, expressions []apiserver.WebhookMatc
defer s.Close() defer s.Close()
defer featuregatetesting.SetFeatureGateDuringTest(b, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, featureEnabled)() defer featuregatetesting.SetFeatureGateDuringTest(b, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, featureEnabled)()
// Create an authorizer with or without expressions to compile // 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(), expressions, "")
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
@ -1259,7 +1369,7 @@ func TestV1WebhookMatchConditions(t *testing.T) {
for i, test := range tests { for i, test := range tests {
t.Run(test.name, func(t *testing.T) { 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(), test.expressions, "")
if len(test.expectedCompileErr) > 0 && err == nil { if len(test.expectedCompileErr) > 0 && err == nil {
t.Fatalf("%d: Expected compile error", i) t.Fatalf("%d: Expected compile error", i)
} else if len(test.expectedCompileErr) == 0 && err != nil { } else if len(test.expectedCompileErr) == 0 && err != nil {
@ -1292,9 +1402,17 @@ func TestV1WebhookMatchConditions(t *testing.T) {
} }
} }
func noopAuthorizerMetrics() AuthorizerMetrics { func noopAuthorizerMetrics() metrics.AuthorizerMetrics {
return AuthorizerMetrics{ return metrics.NoopAuthorizerMetrics{}
RecordRequestTotal: noopMetrics{}.RecordRequestTotal, }
RecordRequestLatency: noopMetrics{}.RecordRequestLatency,
func celAuthorizerMetrics() metrics.AuthorizerMetrics {
return celAuthorizerMetricsType{
MatcherMetrics: celmetrics.NewMatcherMetrics(),
} }
} }
type celAuthorizerMetricsType struct {
metrics.NoopRequestMetrics
celmetrics.MatcherMetrics
}

View File

@ -197,7 +197,7 @@ current-context: default
if err != nil { if err != nil {
return fmt.Errorf("error building sar client: %v", err) 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(), "")
return err return err
}() }()
if err != nil && !tt.wantErr { if err != nil && !tt.wantErr {
@ -340,7 +340,7 @@ func newV1beta1Authorizer(callbackURL string, clientCert, clientKey, ca []byte,
if err != nil { if err != nil {
return nil, fmt.Errorf("error building sar client: %v", err) 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(), "")
} }
func TestV1beta1TLSConfig(t *testing.T) { func TestV1beta1TLSConfig(t *testing.T) {

View File

@ -36,6 +36,7 @@ import (
rbacv1 "k8s.io/api/rbac/v1" rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
celmetrics "k8s.io/apiserver/pkg/authorization/cel"
authorizationmetrics "k8s.io/apiserver/pkg/authorization/metrics" authorizationmetrics "k8s.io/apiserver/pkg/authorization/metrics"
"k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/features"
authzmetrics "k8s.io/apiserver/pkg/server/options/authorizationconfig/metrics" authzmetrics "k8s.io/apiserver/pkg/server/options/authorizationconfig/metrics"
@ -275,40 +276,50 @@ users:
serverAllowCalled.Store(0) serverAllowCalled.Store(0)
serverAllowReloadedCalled.Store(0) serverAllowReloadedCalled.Store(0)
authorizationmetrics.ResetMetricsForTest() authorizationmetrics.ResetMetricsForTest()
celmetrics.ResetMetricsForTest()
} }
var adminClient *clientset.Clientset var adminClient *clientset.Clientset
assertCounts := func(errorCount, timeoutCount, denyCount, noOpinionCount, allowCount, allowReloadedCount int32) { type counts struct {
errorCount, timeoutCount, denyCount, noOpinionCount, allowCount, allowReloadedCount, webhookExclusionCount, evalErrorsCount int32
}
assertCounts := func(c counts) {
t.Helper() t.Helper()
metrics, err := getMetrics(t, adminClient) metrics, err := getMetrics(t, adminClient)
if err != nil { if err != nil {
t.Errorf("error getting metrics: %v", err) t.Fatalf("error getting metrics: %v", err)
} }
if e, a := errorCount, serverErrorCalled.Load(); e != a { if e, a := c.errorCount, serverErrorCalled.Load(); e != a {
t.Errorf("expected fail webhook calls: %d, got %d", e, a) t.Fatalf("expected fail webhook calls: %d, got %d", e, a)
} }
if e, a := timeoutCount, serverTimeoutCalled.Load(); e != a { if e, a := c.timeoutCount, serverTimeoutCalled.Load(); e != a {
t.Errorf("expected timeout webhook calls: %d, got %d", e, a) t.Fatalf("expected timeout webhook calls: %d, got %d", e, a)
} }
if e, a := denyCount, serverDenyCalled.Load(); e != a { if e, a := c.denyCount, serverDenyCalled.Load(); e != a {
t.Errorf("expected deny webhook calls: %d, got %d", e, a) t.Fatalf("expected deny webhook calls: %d, got %d", e, a)
} }
if e, a := denyCount, metrics.decisions[authorizerKey{authorizerType: "Webhook", authorizerName: denyName}]["denied"]; e != int32(a) { if e, a := c.denyCount, metrics.decisions[authorizerKey{authorizerType: "Webhook", authorizerName: denyName}]["denied"]; e != int32(a) {
t.Errorf("expected deny webhook denied metrics calls: %d, got %d", e, a) t.Fatalf("expected deny webhook denied metrics calls: %d, got %d", e, a)
} }
if e, a := noOpinionCount, serverNoOpinionCalled.Load(); e != a { if e, a := c.noOpinionCount, serverNoOpinionCalled.Load(); e != a {
t.Errorf("expected noOpinion webhook calls: %d, got %d", e, a) t.Fatalf("expected noOpinion webhook calls: %d, got %d", e, a)
} }
if e, a := allowCount, serverAllowCalled.Load(); e != a { if e, a := c.allowCount, serverAllowCalled.Load(); e != a {
t.Errorf("expected allow webhook calls: %d, got %d", e, a) t.Fatalf("expected allow webhook calls: %d, got %d", e, a)
} }
if e, a := allowCount, metrics.decisions[authorizerKey{authorizerType: "Webhook", authorizerName: allowName}]["allowed"]; e != int32(a) { if e, a := c.allowCount, metrics.decisions[authorizerKey{authorizerType: "Webhook", authorizerName: allowName}]["allowed"]; e != int32(a) {
t.Errorf("expected allow webhook allowed metrics calls: %d, got %d", e, a) t.Fatalf("expected allow webhook allowed metrics calls: %d, got %d", e, a)
} }
if e, a := allowReloadedCount, serverAllowReloadedCalled.Load(); e != a { if e, a := c.allowReloadedCount, serverAllowReloadedCalled.Load(); e != a {
t.Errorf("expected allowReloaded webhook calls: %d, got %d", e, a) t.Fatalf("expected allowReloaded webhook calls: %d, got %d", e, a)
} }
if e, a := allowReloadedCount, metrics.decisions[authorizerKey{authorizerType: "Webhook", authorizerName: allowReloadedName}]["allowed"]; e != int32(a) { if e, a := c.allowReloadedCount, metrics.decisions[authorizerKey{authorizerType: "Webhook", authorizerName: allowReloadedName}]["allowed"]; e != int32(a) {
t.Errorf("expected allowReloaded webhook allowed metrics calls: %d, got %d", e, a) t.Fatalf("expected allowReloaded webhook allowed metrics calls: %d, got %d", e, a)
}
if e, a := c.webhookExclusionCount, metrics.exclusions; e != int32(a) {
t.Fatalf("expected webhook exclusions due to match conditions: %d, got %d", e, a)
}
if e, a := c.evalErrorsCount, metrics.evalErrors; e != int32(a) {
t.Fatalf("expected webhook match condition eval errors: %d, got %d", e, a)
} }
resetCounts() resetCounts()
} }
@ -348,7 +359,8 @@ authorizers:
type: KubeConfigFile type: KubeConfigFile
kubeConfigFile: `+serverTimeoutKubeconfigName+` kubeConfigFile: `+serverTimeoutKubeconfigName+`
matchConditions: matchConditions:
- expression: has(request.resourceAttributes) # intentionally skip this check so we can trigger an eval error with a non-resource request
# - expression: has(request.resourceAttributes)
- expression: 'request.resourceAttributes.namespace == "fail"' - expression: 'request.resourceAttributes.namespace == "fail"'
- expression: 'request.resourceAttributes.name == "timeout"' - expression: 'request.resourceAttributes.name == "timeout"'
@ -423,7 +435,7 @@ authorizers:
t.Fatal("expected denied, got allowed") t.Fatal("expected denied, got allowed")
} else { } else {
t.Log(result.Status.Reason) t.Log(result.Status.Reason)
assertCounts(1, 0, 0, 0, 0, 0) assertCounts(counts{errorCount: 1})
} }
// timeout webhook short circuits // timeout webhook short circuits
@ -444,7 +456,7 @@ authorizers:
t.Fatal("expected denied, got allowed") t.Fatal("expected denied, got allowed")
} else { } else {
t.Log(result.Status.Reason) t.Log(result.Status.Reason)
assertCounts(0, 1, 0, 0, 0, 0) assertCounts(counts{timeoutCount: 1, webhookExclusionCount: 1})
} }
// deny webhook short circuits // deny webhook short circuits
@ -465,7 +477,7 @@ authorizers:
t.Fatal("expected denied, got allowed") t.Fatal("expected denied, got allowed")
} else { } else {
t.Log(result.Status.Reason) t.Log(result.Status.Reason)
assertCounts(0, 0, 1, 0, 0, 0) assertCounts(counts{denyCount: 1, webhookExclusionCount: 2})
} }
// no-opinion webhook passes through, allow webhook allows // no-opinion webhook passes through, allow webhook allows
@ -486,7 +498,26 @@ authorizers:
t.Fatal("expected allowed, got denied") t.Fatal("expected allowed, got denied")
} else { } else {
t.Log(result.Status.Reason) t.Log(result.Status.Reason)
assertCounts(0, 0, 0, 1, 1, 0) assertCounts(counts{noOpinionCount: 1, allowCount: 1, webhookExclusionCount: 3})
}
// the timeout webhook results in match condition eval errors when evaluating a non-resource request
// failure policy is deny
t.Log("checking match condition eval error")
if result, err := adminClient.AuthorizationV1().SubjectAccessReviews().Create(context.TODO(), &authorizationv1.SubjectAccessReview{Spec: authorizationv1.SubjectAccessReviewSpec{
User: "alice",
NonResourceAttributes: &authorizationv1.NonResourceAttributes{
Verb: "list",
},
}}, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
} else if result.Status.Allowed {
t.Fatal("expected denied, got allowed")
} else {
t.Log(result.Status.Reason)
// error webhook matchConditions skip non-resource request
// timeout webhook matchConditions error on non-resource request
assertCounts(counts{webhookExclusionCount: 1, evalErrorsCount: 1})
} }
// check last loaded success/failure metric timestamps, ensure success is present, failure is not // check last loaded success/failure metric timestamps, ensure success is present, failure is not
@ -550,7 +581,7 @@ authorizers:
t.Fatal("expected allowed, got denied") t.Fatal("expected allowed, got denied")
} else { } else {
t.Log(result.Status.Reason) t.Log(result.Status.Reason)
assertCounts(0, 0, 0, 1, 1, 0) assertCounts(counts{noOpinionCount: 1, allowCount: 1, webhookExclusionCount: 3})
} }
// write good config with different webhook // write good config with different webhook
@ -621,7 +652,7 @@ authorizers:
t.Fatal("expected allowed, got denied") t.Fatal("expected allowed, got denied")
} else { } else {
t.Log(result.Status.Reason) t.Log(result.Status.Reason)
assertCounts(0, 0, 0, 0, 0, 1) assertCounts(counts{allowReloadedCount: 1})
} }
// delete file (do this test last because it makes file watch fall back to one minute poll interval) // delete file (do this test last because it makes file watch fall back to one minute poll interval)
@ -677,7 +708,7 @@ authorizers:
t.Fatal("expected allowed, got denied") t.Fatal("expected allowed, got denied")
} else { } else {
t.Log(result.Status.Reason) t.Log(result.Status.Reason)
assertCounts(0, 0, 0, 0, 0, 1) assertCounts(counts{allowReloadedCount: 1})
} }
} }
@ -685,6 +716,8 @@ type metrics struct {
reloadSuccess *time.Time reloadSuccess *time.Time
reloadFailure *time.Time reloadFailure *time.Time
decisions map[authorizerKey]map[string]int decisions map[authorizerKey]map[string]int
exclusions int
evalErrors int
} }
type authorizerKey struct { type authorizerKey struct {
authorizerType string authorizerType string
@ -692,6 +725,8 @@ type authorizerKey struct {
} }
var decisionMetric = regexp.MustCompile(`apiserver_authorization_decisions_total\{decision="(.*?)",name="(.*?)",type="(.*?)"\} (\d+)`) var decisionMetric = regexp.MustCompile(`apiserver_authorization_decisions_total\{decision="(.*?)",name="(.*?)",type="(.*?)"\} (\d+)`)
var webhookExclusionMetric = regexp.MustCompile(`apiserver_authorization_match_condition_exclusions_total\{name="(.*?)",type="(.*?)"\} (\d+)`)
var webhookMatchConditionEvalErrorMetric = regexp.MustCompile(`apiserver_authorization_match_condition_evaluation_errors_total\{name="(.*?)",type="(.*?)"\} (\d+)`)
func getMetrics(t *testing.T, client *clientset.Clientset) (*metrics, error) { func getMetrics(t *testing.T, client *clientset.Clientset) (*metrics, error) {
data, err := client.RESTClient().Get().AbsPath("/metrics").DoRaw(context.TODO()) data, err := client.RESTClient().Get().AbsPath("/metrics").DoRaw(context.TODO())
@ -703,11 +738,13 @@ func getMetrics(t *testing.T, client *clientset.Clientset) (*metrics, error) {
// apiserver_authorization_decisions_total{decision="denied",name="deny.example.com",type="Webhook"} 1 // apiserver_authorization_decisions_total{decision="denied",name="deny.example.com",type="Webhook"} 1
// apiserver_authorization_decisions_total{decision="denied",name="error.example.com",type="Webhook"} 1 // apiserver_authorization_decisions_total{decision="denied",name="error.example.com",type="Webhook"} 1
// apiserver_authorization_decisions_total{decision="denied",name="timeout.example.com",type="Webhook"} 1 // apiserver_authorization_decisions_total{decision="denied",name="timeout.example.com",type="Webhook"} 1
// apiserver_authorization_match_condition_exclusions_total{name="exclusion.example.com",type="webhook"} 1
if err != nil { if err != nil {
return nil, err return nil, err
} }
var m metrics var m metrics
m.exclusions = 0
for _, line := range strings.Split(string(data), "\n") { for _, line := range strings.Split(string(data), "\n") {
if matches := decisionMetric.FindStringSubmatch(line); matches != nil { if matches := decisionMetric.FindStringSubmatch(line); matches != nil {
t.Log(line) t.Log(line)
@ -725,6 +762,24 @@ func getMetrics(t *testing.T, client *clientset.Clientset) (*metrics, error) {
m.decisions[key][matches[1]] = count m.decisions[key][matches[1]] = count
} }
if matches := webhookExclusionMetric.FindStringSubmatch(line); matches != nil {
t.Log(matches)
count, err := strconv.Atoi(matches[3])
if err != nil {
return nil, err
}
t.Log(count)
m.exclusions += count
}
if matches := webhookMatchConditionEvalErrorMetric.FindStringSubmatch(line); matches != nil {
t.Log(matches)
count, err := strconv.Atoi(matches[3])
if err != nil {
return nil, err
}
t.Log(count)
m.evalErrors += count
}
if strings.HasPrefix(line, "apiserver_authorization_config_controller_automatic_reload_last_timestamp_seconds") { if strings.HasPrefix(line, "apiserver_authorization_config_controller_automatic_reload_last_timestamp_seconds") {
t.Log(line) t.Log(line)
values := strings.Split(line, " ") values := strings.Split(line, " ")