From fe847b31f4b95c4f74bf35b410e3eaa3807227f5 Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Fri, 16 Feb 2024 02:26:18 -0500 Subject: [PATCH] Add allowed/denied metrics for authorizers Kubernetes-commit: d5d3eddb95b657f03677c21498f185d70d87cdda --- pkg/authorization/metrics/metrics.go | 92 +++++++++++++++ pkg/authorization/metrics/metrics_test.go | 105 ++++++++++++++++++ .../authorizationconfig/metrics/metrics.go | 1 - 3 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 pkg/authorization/metrics/metrics.go create mode 100644 pkg/authorization/metrics/metrics_test.go diff --git a/pkg/authorization/metrics/metrics.go b/pkg/authorization/metrics/metrics.go new file mode 100644 index 000000000..0885891ad --- /dev/null +++ b/pkg/authorization/metrics/metrics.go @@ -0,0 +1,92 @@ +/* +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 metrics + +import ( + "context" + "sync" + + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/component-base/metrics" + "k8s.io/component-base/metrics/legacyregistry" +) + +const ( + namespace = "apiserver" + subsystem = "authorization" +) + +var ( + authorizationDecisionsTotal = metrics.NewCounterVec( + &metrics.CounterOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "decisions_total", + Help: "Total number of terminal decisions made by an authorizer split by authorizer type, name, and decision.", + StabilityLevel: metrics.ALPHA, + }, + []string{"type", "name", "decision"}, + ) +) + +var registerMetrics sync.Once + +func RegisterMetrics() { + registerMetrics.Do(func() { + legacyregistry.MustRegister(authorizationDecisionsTotal) + }) +} + +func ResetMetricsForTest() { + authorizationDecisionsTotal.Reset() +} + +func RecordAuthorizationDecision(authorizerType, authorizerName, decision string) { + authorizationDecisionsTotal.WithLabelValues(authorizerType, authorizerName, decision).Inc() +} + +func InstrumentedAuthorizer(authorizerType string, authorizerName string, delegate authorizer.Authorizer) authorizer.Authorizer { + RegisterMetrics() + return &instrumentedAuthorizer{ + authorizerType: string(authorizerType), + authorizerName: authorizerName, + delegate: delegate, + } +} + +type instrumentedAuthorizer struct { + authorizerType string + authorizerName string + delegate authorizer.Authorizer +} + +func (a *instrumentedAuthorizer) Authorize(ctx context.Context, attributes authorizer.Attributes) (authorizer.Decision, string, error) { + decision, reason, err := a.delegate.Authorize(ctx, attributes) + switch decision { + case authorizer.DecisionNoOpinion: + // non-terminal, not reported + case authorizer.DecisionAllow: + // matches SubjectAccessReview status.allowed field name + RecordAuthorizationDecision(a.authorizerType, a.authorizerName, "allowed") + case authorizer.DecisionDeny: + // matches SubjectAccessReview status.denied field name + RecordAuthorizationDecision(a.authorizerType, a.authorizerName, "denied") + default: + RecordAuthorizationDecision(a.authorizerType, a.authorizerName, "unknown") + } + return decision, reason, err +} diff --git a/pkg/authorization/metrics/metrics_test.go b/pkg/authorization/metrics/metrics_test.go new file mode 100644 index 000000000..a7a7dd6bc --- /dev/null +++ b/pkg/authorization/metrics/metrics_test.go @@ -0,0 +1,105 @@ +/* +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 metrics + +import ( + "context" + "strings" + "testing" + + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/component-base/metrics/legacyregistry" + "k8s.io/component-base/metrics/testutil" +) + +func TestRecordAuthorizationDecisionsTotal(t *testing.T) { + prefix := ` + # HELP apiserver_authorization_decisions_total [ALPHA] Total number of terminal decisions made by an authorizer split by authorizer type, name, and decision. + # TYPE apiserver_authorization_decisions_total counter` + metrics := []string{ + namespace + "_" + subsystem + "_decisions_total", + } + + authorizationDecisionsTotal.Reset() + RegisterMetrics() + + dummyAuthorizer := &dummyAuthorizer{} + a := InstrumentedAuthorizer("mytype", "myname", dummyAuthorizer) + + // allow + { + dummyAuthorizer.decision = authorizer.DecisionAllow + _, _, _ = a.Authorize(context.Background(), nil) + expectedValue := prefix + ` + apiserver_authorization_decisions_total{decision="allowed",name="myname",type="mytype"} 1 + ` + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expectedValue), metrics...); err != nil { + t.Fatal(err) + } + authorizationDecisionsTotal.Reset() + } + + // deny + { + dummyAuthorizer.decision = authorizer.DecisionDeny + _, _, _ = a.Authorize(context.Background(), nil) + _, _, _ = a.Authorize(context.Background(), nil) + expectedValue := prefix + ` + apiserver_authorization_decisions_total{decision="denied",name="myname",type="mytype"} 2 + ` + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expectedValue), metrics...); err != nil { + t.Fatal(err) + } + authorizationDecisionsTotal.Reset() + } + + // no-opinion emits no metric + { + dummyAuthorizer.decision = authorizer.DecisionNoOpinion + _, _, _ = a.Authorize(context.Background(), nil) + _, _, _ = a.Authorize(context.Background(), nil) + expectedValue := prefix + ` + ` + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expectedValue), metrics...); err != nil { + t.Fatal(err) + } + authorizationDecisionsTotal.Reset() + } + + // unknown decision emits a metric + { + dummyAuthorizer.decision = authorizer.DecisionDeny + 10 + _, _, _ = a.Authorize(context.Background(), nil) + expectedValue := prefix + ` + apiserver_authorization_decisions_total{decision="unknown",name="myname",type="mytype"} 1 + ` + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expectedValue), metrics...); err != nil { + t.Fatal(err) + } + authorizationDecisionsTotal.Reset() + } + +} + +type dummyAuthorizer struct { + decision authorizer.Decision + err error +} + +func (d *dummyAuthorizer) Authorize(ctx context.Context, attrs authorizer.Attributes) (authorizer.Decision, string, error) { + return d.decision, "", d.err +} diff --git a/pkg/server/options/authorizationconfig/metrics/metrics.go b/pkg/server/options/authorizationconfig/metrics/metrics.go index 09089348a..2e739b15c 100644 --- a/pkg/server/options/authorizationconfig/metrics/metrics.go +++ b/pkg/server/options/authorizationconfig/metrics/metrics.go @@ -73,7 +73,6 @@ func RegisterMetrics() { func ResetMetricsForTest() { authorizationConfigAutomaticReloadsTotal.Reset() authorizationConfigAutomaticReloadLastTimestampSeconds.Reset() - legacyregistry.Reset() } func RecordAuthorizationConfigAutomaticReloadFailure(apiServerID string) {