diff --git a/plugin/pkg/auth/authorizer/rbac/rbac.go b/plugin/pkg/auth/authorizer/rbac/rbac.go index bb714a01a08..122a10b2f3a 100644 --- a/plugin/pkg/auth/authorizer/rbac/rbac.go +++ b/plugin/pkg/auth/authorizer/rbac/rbac.go @@ -62,7 +62,7 @@ type authorizingVisitor struct { func (v *authorizingVisitor) visit(source fmt.Stringer, rule *rbac.PolicyRule, err error) bool { if rule != nil && RuleAllows(v.requestAttributes, rule) { v.allowed = true - v.reason = fmt.Sprintf("allowed by %s", source.String()) + v.reason = fmt.Sprintf("RBAC: allowed by %s", source.String()) return false } if err != nil { @@ -120,7 +120,7 @@ func (r *RBACAuthorizer) Authorize(requestAttributes authorizer.Attributes) (aut reason := "" if len(ruleCheckingVisitor.errors) > 0 { - reason = fmt.Sprintf("%v", utilerrors.NewAggregate(ruleCheckingVisitor.errors)) + reason = fmt.Sprintf("RBAC: %v", utilerrors.NewAggregate(ruleCheckingVisitor.errors)) } return authorizer.DecisionNoOpinion, reason, nil } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/BUILD index 98dbb32b745..179522ebc52 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/BUILD @@ -20,6 +20,7 @@ go_test( embed = [":go_default_library"], deps = [ "//vendor/github.com/pborman/uuid:go_default_library", + "//vendor/github.com/stretchr/testify/assert:go_default_library", "//vendor/k8s.io/api/authentication/v1:go_default_library", "//vendor/k8s.io/api/batch/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit_test.go index a7c1486a700..f8086ba7ca0 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit_test.go @@ -736,7 +736,8 @@ func TestAudit(t *testing.T) { } type fakeRequestContextMapper struct { - user *user.DefaultInfo + user *user.DefaultInfo + audit *auditinternal.Event } func (m *fakeRequestContextMapper) Get(req *http.Request) (request.Context, bool) { @@ -744,6 +745,9 @@ func (m *fakeRequestContextMapper) Get(req *http.Request) (request.Context, bool if m.user != nil { ctx = request.WithUser(ctx, m.user) } + if m.audit != nil { + ctx = request.WithAuditEvent(ctx, m.audit) + } resolver := newTestRequestInfoResolver() info, err := resolver.NewRequestInfo(req) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authorization.go b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authorization.go index b4610dfcea6..6de62bde5e9 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authorization.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authorization.go @@ -23,11 +23,23 @@ import ( "github.com/golang/glog" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/audit" "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" "k8s.io/apiserver/pkg/endpoints/request" ) +const ( + // Annotation key names set in advanced audit + decisionAnnotationKey = "authorization.k8s.io/decision" + reasonAnnotationKey = "authorization.k8s.io/reason" + + // Annotation values set in advanced audit + decisionAllow = "allow" + decisionForbid = "forbid" + reasonError = "internal error" +) + // WithAuthorizationCheck passes all authorized requests on to handler, and returns a forbidden error otherwise. func WithAuthorization(handler http.Handler, requestContextMapper request.RequestContextMapper, a authorizer.Authorizer, s runtime.NegotiatedSerializer) http.Handler { if a == nil { @@ -40,6 +52,7 @@ func WithAuthorization(handler http.Handler, requestContextMapper request.Reques responsewriters.InternalError(w, req, errors.New("no context found for request")) return } + ae := request.AuditEventFrom(ctx) attributes, err := GetAuthorizerAttributes(ctx) if err != nil { @@ -49,15 +62,20 @@ func WithAuthorization(handler http.Handler, requestContextMapper request.Reques authorized, reason, err := a.Authorize(attributes) // 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 { + audit.LogAnnotation(ae, decisionAnnotationKey, decisionAllow) + audit.LogAnnotation(ae, reasonAnnotationKey, reason) handler.ServeHTTP(w, req) return } if err != nil { + audit.LogAnnotation(ae, reasonAnnotationKey, reasonError) responsewriters.InternalError(w, req, err) return } glog.V(4).Infof("Forbidden: %#v, Reason: %q", req.RequestURI, reason) + audit.LogAnnotation(ae, decisionAnnotationKey, decisionForbid) + audit.LogAnnotation(ae, reasonAnnotationKey, reason) responsewriters.Forbidden(ctx, attributes, w, req, reason, s) }) } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authorization_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authorization_test.go index 477e5d4e3ed..9eba3a5a905 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authorization_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authorization_test.go @@ -23,7 +23,11 @@ import ( "reflect" "testing" + "github.com/stretchr/testify/assert" batch "k8s.io/api/batch/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + auditinternal "k8s.io/apiserver/pkg/apis/audit" "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" "k8s.io/apiserver/pkg/endpoints/request" @@ -127,3 +131,65 @@ func TestGetAuthorizerAttributes(t *testing.T) { } } } + +type fakeAuthorizer struct { + decision authorizer.Decision + reason string + err error +} + +func (f fakeAuthorizer) Authorize(a authorizer.Attributes) (authorizer.Decision, string, error) { + return f.decision, f.reason, f.err +} + +func TestAuditAnnotation(t *testing.T) { + testcases := map[string]struct { + authorizer fakeAuthorizer + decisionAnnotation string + reasonAnnotation string + }{ + "decision allow": { + fakeAuthorizer{ + authorizer.DecisionAllow, + "RBAC: allowed to patch pod", + nil, + }, + "allow", + "RBAC: allowed to patch pod", + }, + "decision forbid": { + fakeAuthorizer{ + authorizer.DecisionDeny, + "RBAC: not allowed to patch pod", + nil, + }, + "forbid", + "RBAC: not allowed to patch pod", + }, + "error": { + fakeAuthorizer{ + authorizer.DecisionNoOpinion, + "", + errors.New("can't parse user info"), + }, + "", + reasonError, + }, + } + + scheme := runtime.NewScheme() + negotiatedSerializer := serializer.DirectCodecFactory{CodecFactory: serializer.NewCodecFactory(scheme)} + for k, tc := range testcases { + audit := &auditinternal.Event{Level: auditinternal.LevelMetadata} + handler := WithAuthorization(&fakeHTTPHandler{}, &fakeRequestContextMapper{ + audit: audit, + }, tc.authorizer, negotiatedSerializer) + + req, _ := http.NewRequest("GET", "/api/v1/namespaces/default/pods", nil) + req.RemoteAddr = "127.0.0.1" + handler.ServeHTTP(httptest.NewRecorder(), req) + assert.Equal(t, tc.decisionAnnotation, audit.Annotations[decisionAnnotationKey], k+": unexpected decision annotation") + assert.Equal(t, tc.reasonAnnotation, audit.Annotations[reasonAnnotationKey], k+": unexpected reason annotation") + } + +}