mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-05 18:24:07 +00:00
Merge pull request #117211 from HirazawaUi/add-auth-metrics
add Authorization tracking request/error counts and latency metrics
This commit is contained in:
commit
7e25f1232a
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user