Merge pull request #117211 from HirazawaUi/add-auth-metrics
add Authorization tracking request/error counts and latency metrics
This commit is contained in:
		@@ -34,17 +34,17 @@ import (
 | 
				
			|||||||
	"k8s.io/klog/v2"
 | 
						"k8s.io/klog/v2"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type recordMetrics func(context.Context, *authenticator.Response, bool, error, authenticator.Audiences, time.Time, time.Time)
 | 
					type authenticationRecordMetricsFunc func(context.Context, *authenticator.Response, bool, error, authenticator.Audiences, time.Time, time.Time)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// WithAuthentication creates an http handler that tries to authenticate the given request as a user, and then
 | 
					// WithAuthentication creates an http handler that tries to authenticate the given request as a user, and then
 | 
				
			||||||
// stores any such user found onto the provided context for the request. If authentication fails or returns an error
 | 
					// stores any such user found onto the provided context for the request. If authentication fails or returns an error
 | 
				
			||||||
// the failed handler is used. On success, "Authorization" header is removed from the request and handler
 | 
					// the failed handler is used. On success, "Authorization" header is removed from the request and handler
 | 
				
			||||||
// is invoked to serve the request.
 | 
					// is invoked to serve the request.
 | 
				
			||||||
func WithAuthentication(handler http.Handler, auth authenticator.Request, failed http.Handler, apiAuds authenticator.Audiences, requestHeaderConfig *authenticatorfactory.RequestHeaderConfig) http.Handler {
 | 
					func WithAuthentication(handler http.Handler, auth authenticator.Request, failed http.Handler, apiAuds authenticator.Audiences, requestHeaderConfig *authenticatorfactory.RequestHeaderConfig) http.Handler {
 | 
				
			||||||
	return withAuthentication(handler, auth, failed, apiAuds, requestHeaderConfig, recordAuthMetrics)
 | 
						return withAuthentication(handler, auth, failed, apiAuds, requestHeaderConfig, recordAuthenticationMetrics)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func withAuthentication(handler http.Handler, auth authenticator.Request, failed http.Handler, apiAuds authenticator.Audiences, requestHeaderConfig *authenticatorfactory.RequestHeaderConfig, metrics recordMetrics) http.Handler {
 | 
					func withAuthentication(handler http.Handler, auth authenticator.Request, failed http.Handler, apiAuds authenticator.Audiences, requestHeaderConfig *authenticatorfactory.RequestHeaderConfig, metrics authenticationRecordMetricsFunc) http.Handler {
 | 
				
			||||||
	if auth == nil {
 | 
						if auth == nil {
 | 
				
			||||||
		klog.Warning("Authentication is disabled")
 | 
							klog.Warning("Authentication is disabled")
 | 
				
			||||||
		return handler
 | 
							return handler
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,7 @@ import (
 | 
				
			|||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"k8s.io/klog/v2"
 | 
						"k8s.io/klog/v2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -41,14 +42,21 @@ const (
 | 
				
			|||||||
	reasonError    = "internal error"
 | 
						reasonError    = "internal error"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// WithAuthorizationCheck passes all authorized requests on to handler, and returns a forbidden error otherwise.
 | 
					type recordAuthorizationMetricsFunc func(ctx context.Context, authorized authorizer.Decision, err error, authStart time.Time, authFinish time.Time)
 | 
				
			||||||
func WithAuthorization(handler http.Handler, a authorizer.Authorizer, s runtime.NegotiatedSerializer) http.Handler {
 | 
					
 | 
				
			||||||
 | 
					// WithAuthorization passes all authorized requests on to handler, and returns a forbidden error otherwise.
 | 
				
			||||||
 | 
					func WithAuthorization(hhandler http.Handler, auth authorizer.Authorizer, s runtime.NegotiatedSerializer) http.Handler {
 | 
				
			||||||
 | 
						return withAuthorization(hhandler, auth, s, recordAuthorizationMetrics)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func withAuthorization(handler http.Handler, a authorizer.Authorizer, s runtime.NegotiatedSerializer, metrics recordAuthorizationMetricsFunc) http.Handler {
 | 
				
			||||||
	if a == nil {
 | 
						if a == nil {
 | 
				
			||||||
		klog.Warning("Authorization is disabled")
 | 
							klog.Warning("Authorization is disabled")
 | 
				
			||||||
		return handler
 | 
							return handler
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 | 
						return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 | 
				
			||||||
		ctx := req.Context()
 | 
							ctx := req.Context()
 | 
				
			||||||
 | 
							authorizationStart := time.Now()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		attributes, err := GetAuthorizerAttributes(ctx)
 | 
							attributes, err := GetAuthorizerAttributes(ctx)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
@@ -56,6 +64,12 @@ func WithAuthorization(handler http.Handler, a authorizer.Authorizer, s runtime.
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		authorized, reason, err := a.Authorize(ctx, attributes)
 | 
							authorized, reason, err := a.Authorize(ctx, attributes)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							authorizationFinish := time.Now()
 | 
				
			||||||
 | 
							defer func() {
 | 
				
			||||||
 | 
								metrics(ctx, authorized, err, authorizationStart, authorizationFinish)
 | 
				
			||||||
 | 
							}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// an authorizer like RBAC could encounter evaluation errors and still allow the request, so authorizer decision is checked before error here.
 | 
							// an authorizer like RBAC could encounter evaluation errors and still allow the request, so authorizer decision is checked before error here.
 | 
				
			||||||
		if authorized == authorizer.DecisionAllow {
 | 
							if authorized == authorizer.DecisionAllow {
 | 
				
			||||||
			audit.AddAuditAnnotations(ctx,
 | 
								audit.AddAuditAnnotations(ctx,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,6 +21,8 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/authorization/authorizer"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"k8s.io/apiserver/pkg/authentication/authenticator"
 | 
						"k8s.io/apiserver/pkg/authentication/authenticator"
 | 
				
			||||||
	"k8s.io/component-base/metrics"
 | 
						"k8s.io/component-base/metrics"
 | 
				
			||||||
	"k8s.io/component-base/metrics/legacyregistry"
 | 
						"k8s.io/component-base/metrics/legacyregistry"
 | 
				
			||||||
@@ -38,6 +40,10 @@ const (
 | 
				
			|||||||
	successLabel = "success"
 | 
						successLabel = "success"
 | 
				
			||||||
	failureLabel = "failure"
 | 
						failureLabel = "failure"
 | 
				
			||||||
	errorLabel   = "error"
 | 
						errorLabel   = "error"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						allowedLabel   = "allowed"
 | 
				
			||||||
 | 
						deniedLabel    = "denied"
 | 
				
			||||||
 | 
						noOpinionLabel = "no-opinion"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
@@ -68,15 +74,54 @@ var (
 | 
				
			|||||||
		},
 | 
							},
 | 
				
			||||||
		[]string{"result"},
 | 
							[]string{"result"},
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						authorizationAttemptsCounter = metrics.NewCounterVec(
 | 
				
			||||||
 | 
							&metrics.CounterOpts{
 | 
				
			||||||
 | 
								Name:           "authorization_attempts_total",
 | 
				
			||||||
 | 
								Help:           "Counter of authorization attempts broken down by result. It can be either 'allowed', 'denied', 'no-opinion' or 'error'.",
 | 
				
			||||||
 | 
								StabilityLevel: metrics.ALPHA,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							[]string{"result"},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						authorizationLatency = metrics.NewHistogramVec(
 | 
				
			||||||
 | 
							&metrics.HistogramOpts{
 | 
				
			||||||
 | 
								Name:           "authorization_duration_seconds",
 | 
				
			||||||
 | 
								Help:           "Authorization duration in seconds broken out by result.",
 | 
				
			||||||
 | 
								Buckets:        metrics.ExponentialBuckets(0.001, 2, 15),
 | 
				
			||||||
 | 
								StabilityLevel: metrics.ALPHA,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							[]string{"result"},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func init() {
 | 
					func init() {
 | 
				
			||||||
	legacyregistry.MustRegister(authenticatedUserCounter)
 | 
						legacyregistry.MustRegister(authenticatedUserCounter)
 | 
				
			||||||
	legacyregistry.MustRegister(authenticatedAttemptsCounter)
 | 
						legacyregistry.MustRegister(authenticatedAttemptsCounter)
 | 
				
			||||||
	legacyregistry.MustRegister(authenticationLatency)
 | 
						legacyregistry.MustRegister(authenticationLatency)
 | 
				
			||||||
 | 
						legacyregistry.MustRegister(authorizationAttemptsCounter)
 | 
				
			||||||
 | 
						legacyregistry.MustRegister(authorizationLatency)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func recordAuthMetrics(ctx context.Context, resp *authenticator.Response, ok bool, err error, apiAudiences authenticator.Audiences, authStart time.Time, authFinish time.Time) {
 | 
					func recordAuthorizationMetrics(ctx context.Context, authorized authorizer.Decision, err error, authStart time.Time, authFinish time.Time) {
 | 
				
			||||||
 | 
						var resultLabel string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case authorized == authorizer.DecisionAllow:
 | 
				
			||||||
 | 
							resultLabel = allowedLabel
 | 
				
			||||||
 | 
						case err != nil:
 | 
				
			||||||
 | 
							resultLabel = errorLabel
 | 
				
			||||||
 | 
						case authorized == authorizer.DecisionDeny:
 | 
				
			||||||
 | 
							resultLabel = deniedLabel
 | 
				
			||||||
 | 
						case authorized == authorizer.DecisionNoOpinion:
 | 
				
			||||||
 | 
							resultLabel = noOpinionLabel
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						authorizationAttemptsCounter.WithContext(ctx).WithLabelValues(resultLabel).Inc()
 | 
				
			||||||
 | 
						authorizationLatency.WithContext(ctx).WithLabelValues(resultLabel).Observe(authFinish.Sub(authStart).Seconds())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func recordAuthenticationMetrics(ctx context.Context, resp *authenticator.Response, ok bool, err error, apiAudiences authenticator.Audiences, authStart time.Time, authFinish time.Time) {
 | 
				
			||||||
	var resultLabel string
 | 
						var resultLabel string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	switch {
 | 
						switch {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,8 +23,12 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/runtime"
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/runtime/serializer"
 | 
				
			||||||
 | 
						auditinternal "k8s.io/apiserver/pkg/apis/audit"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/authentication/authenticator"
 | 
						"k8s.io/apiserver/pkg/authentication/authenticator"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/authentication/user"
 | 
						"k8s.io/apiserver/pkg/authentication/user"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/authorization/authorizer"
 | 
				
			||||||
	"k8s.io/component-base/metrics/legacyregistry"
 | 
						"k8s.io/component-base/metrics/legacyregistry"
 | 
				
			||||||
	"k8s.io/component-base/metrics/testutil"
 | 
						"k8s.io/component-base/metrics/testutil"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -158,3 +162,110 @@ func TestMetrics(t *testing.T) {
 | 
				
			|||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestRecordAuthorizationMetricsMetrics(t *testing.T) {
 | 
				
			||||||
 | 
						// Excluding authorization_duration_seconds since it is difficult to predict its values.
 | 
				
			||||||
 | 
						metrics := []string{
 | 
				
			||||||
 | 
							"authorization_attempts_total",
 | 
				
			||||||
 | 
							"authorization_decision_annotations_total",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						testCases := []struct {
 | 
				
			||||||
 | 
							desc       string
 | 
				
			||||||
 | 
							authorizer fakeAuthorizer
 | 
				
			||||||
 | 
							want       string
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								desc: "auth ok",
 | 
				
			||||||
 | 
								authorizer: fakeAuthorizer{
 | 
				
			||||||
 | 
									authorizer.DecisionAllow,
 | 
				
			||||||
 | 
									"RBAC: allowed to patch pod",
 | 
				
			||||||
 | 
									nil,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								want: `
 | 
				
			||||||
 | 
								# HELP authorization_attempts_total [ALPHA] Counter of authorization attempts broken down by result. It can be either 'allowed', 'denied', 'no-opinion' or 'error'.
 | 
				
			||||||
 | 
								# TYPE authorization_attempts_total counter
 | 
				
			||||||
 | 
								authorization_attempts_total{result="allowed"} 1
 | 
				
			||||||
 | 
									`,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								desc: "decision forbid",
 | 
				
			||||||
 | 
								authorizer: fakeAuthorizer{
 | 
				
			||||||
 | 
									authorizer.DecisionDeny,
 | 
				
			||||||
 | 
									"RBAC: not allowed to patch pod",
 | 
				
			||||||
 | 
									nil,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								want: `
 | 
				
			||||||
 | 
								# HELP authorization_attempts_total [ALPHA] Counter of authorization attempts broken down by result. It can be either 'allowed', 'denied', 'no-opinion' or 'error'.
 | 
				
			||||||
 | 
								# TYPE authorization_attempts_total counter
 | 
				
			||||||
 | 
								authorization_attempts_total{result="denied"} 1
 | 
				
			||||||
 | 
									`,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								desc: "authorizer failed with error",
 | 
				
			||||||
 | 
								authorizer: fakeAuthorizer{
 | 
				
			||||||
 | 
									authorizer.DecisionNoOpinion,
 | 
				
			||||||
 | 
									"",
 | 
				
			||||||
 | 
									errors.New("can't parse user info"),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								want: `
 | 
				
			||||||
 | 
								# HELP authorization_attempts_total [ALPHA] Counter of authorization attempts broken down by result. It can be either 'allowed', 'denied', 'no-opinion' or 'error'.
 | 
				
			||||||
 | 
								# TYPE authorization_attempts_total counter
 | 
				
			||||||
 | 
								authorization_attempts_total{result="error"} 1
 | 
				
			||||||
 | 
									`,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								desc: "authorizer decided allow with error",
 | 
				
			||||||
 | 
								authorizer: fakeAuthorizer{
 | 
				
			||||||
 | 
									authorizer.DecisionAllow,
 | 
				
			||||||
 | 
									"",
 | 
				
			||||||
 | 
									errors.New("can't parse user info"),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								want: `
 | 
				
			||||||
 | 
								# HELP authorization_attempts_total [ALPHA] Counter of authorization attempts broken down by result. It can be either 'allowed', 'denied', 'no-opinion' or 'error'.
 | 
				
			||||||
 | 
								# TYPE authorization_attempts_total counter
 | 
				
			||||||
 | 
								authorization_attempts_total{result="allowed"} 1
 | 
				
			||||||
 | 
									`,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								desc: "authorizer failed with error",
 | 
				
			||||||
 | 
								authorizer: fakeAuthorizer{
 | 
				
			||||||
 | 
									authorizer.DecisionNoOpinion,
 | 
				
			||||||
 | 
									"",
 | 
				
			||||||
 | 
									nil,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								want: `
 | 
				
			||||||
 | 
								# HELP authorization_attempts_total [ALPHA] Counter of authorization attempts broken down by result. It can be either 'allowed', 'denied', 'no-opinion' or 'error'.
 | 
				
			||||||
 | 
								# TYPE authorization_attempts_total counter
 | 
				
			||||||
 | 
								authorization_attempts_total{result="no-opinion"} 1
 | 
				
			||||||
 | 
									`,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Since prometheus' gatherer is global, other tests may have updated metrics already, so
 | 
				
			||||||
 | 
						// we need to reset them prior running this test.
 | 
				
			||||||
 | 
						// This also implies that we can't run this test in parallel with other auth tests.
 | 
				
			||||||
 | 
						authorizationAttemptsCounter.Reset()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						scheme := runtime.NewScheme()
 | 
				
			||||||
 | 
						negotiatedSerializer := serializer.NewCodecFactory(scheme).WithoutConversion()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tt := range testCases {
 | 
				
			||||||
 | 
							t.Run(tt.desc, func(t *testing.T) {
 | 
				
			||||||
 | 
								defer authorizationAttemptsCounter.Reset()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								audit := &auditinternal.Event{Level: auditinternal.LevelMetadata}
 | 
				
			||||||
 | 
								handler := WithAuthorization(&fakeHTTPHandler{}, tt.authorizer, negotiatedSerializer)
 | 
				
			||||||
 | 
								// TODO: fake audit injector
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req, _ := http.NewRequest("GET", "/api/v1/namespaces/default/pods", nil)
 | 
				
			||||||
 | 
								req = withTestContext(req, nil, audit)
 | 
				
			||||||
 | 
								req.RemoteAddr = "127.0.0.1"
 | 
				
			||||||
 | 
								handler.ServeHTTP(httptest.NewRecorder(), req)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tt.want), metrics...); err != nil {
 | 
				
			||||||
 | 
									t.Fatal(err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user