apiserver: store (event, evaluated policy) pair in request context

This commit is contained in:
Abu Kashem 2021-09-20 17:43:16 -04:00
parent 91f820eee4
commit 8be823b0b0
No known key found for this signature in database
GPG Key ID: 33A4FA7088DB68A9
17 changed files with 81 additions and 39 deletions

View File

@ -19,6 +19,7 @@ package audit
import (
"context"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
)
@ -27,7 +28,15 @@ type key int
const (
// auditAnnotationsKey is the context key for the audit annotations.
// TODO: it's wasteful to store the audit annotations under a separate key, we
// copy the request context twice for audit purposes. We should move the audit
// annotations under AuditContext so we can get rid of the additional request
// context copy.
auditAnnotationsKey key = iota
// auditKey is the context key for storing the audit event that is being
// captured and the evaluated policy that applies to the given request.
auditKey
)
// annotations = *[]annotation instead of a map to preserve order of insertions
@ -59,7 +68,7 @@ func WithAuditAnnotations(parent context.Context) context.Context {
// prefer AddAuditAnnotation over LogAnnotation to avoid dropping annotations.
func AddAuditAnnotation(ctx context.Context, key, value string) {
// use the audit event directly if we have it
if ae := genericapirequest.AuditEventFrom(ctx); ae != nil {
if ae := AuditEventFrom(ctx); ae != nil {
LogAnnotation(ae, key, value)
return
}
@ -82,3 +91,26 @@ func auditAnnotationsFrom(ctx context.Context) []annotation {
return *annotations
}
// WithAuditContext returns a new context that stores the pair of the audit
// configuration object that applies to the given request and
// the audit event that is going to be written to the API audit log.
func WithAuditContext(parent context.Context, ev *AuditContext) context.Context {
return genericapirequest.WithValue(parent, auditKey, ev)
}
// AuditEventFrom returns the audit event struct on the ctx
func AuditEventFrom(ctx context.Context) *auditinternal.Event {
if o := AuditContextFrom(ctx); o != nil {
return o.Event
}
return nil
}
// AuditContextFrom returns the pair of the audit configuration object
// that applies to the given request and the audit event that is going to
// be written to the API audit log.
func AuditContextFrom(ctx context.Context) *AuditContext {
ev, _ := ctx.Value(auditKey).(*AuditContext)
return ev
}

View File

@ -21,6 +21,23 @@ import (
"k8s.io/apiserver/pkg/authorization/authorizer"
)
// AuditContext is a pair of the audit configuration object that applies to
// a given request and the audit Event object that is being captured.
// It's a convenient placeholder to store both these objects in the request context.
type AuditContext struct {
// RequestAuditConfig is the audit configuration that applies to the request
RequestAuditConfig RequestAuditConfig
// Event is the audit Event object that is being captured to be written in
// the API audit log. It is set to nil when the request is not being audited.
Event *audit.Event
}
// RequestAuditConfig is the evaluated audit configuration that is applicable to
// a given request. PolicyRuleEvaluator evaluates the audit policy against the
// authorizer attributes and returns a RequestAuditConfig that applies to the request.
type RequestAuditConfig struct{}
// PolicyRuleEvaluator exposes methods for evaluating the policy rules.
type PolicyRuleEvaluator interface {
// Check the audit level for a request with the given authorizer attributes.

View File

@ -36,7 +36,6 @@ import (
auditinternal "k8s.io/apiserver/pkg/apis/audit"
"k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/klog/v2"
"k8s.io/utils/clock"
)
@ -189,7 +188,7 @@ func (a *cachedTokenAuthenticator) doAuthenticateToken(ctx context.Context, toke
// since this is shared work between multiple requests, we have no way of knowing if any
// particular request supports audit annotations. thus we always attempt to record them.
ev := &auditinternal.Event{Level: auditinternal.LevelMetadata}
ctx = request.WithAuditEvent(ctx, ev)
ctx = audit.WithAuditContext(ctx, &audit.AuditContext{Event: ev})
record.resp, record.ok, record.err = a.authenticator.AuthenticateToken(ctx, token)
record.annotations = ev.Annotations

View File

@ -36,7 +36,6 @@ import (
"k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/utils/clock"
testingclock "k8s.io/utils/clock/testing"
)
@ -309,7 +308,9 @@ func TestCachedAuditAnnotations(t *testing.T) {
if randomChoice {
ctx = audit.WithAuditAnnotations(ctx)
} else {
ctx = request.WithAuditEvent(ctx, &auditinternal.Event{Level: auditinternal.LevelMetadata})
ctx = audit.WithAuditContext(ctx, &audit.AuditContext{
Event: &auditinternal.Event{Level: auditinternal.LevelMetadata},
})
}
_, _, _ = a.AuthenticateToken(ctx, "token")
@ -317,7 +318,7 @@ func TestCachedAuditAnnotations(t *testing.T) {
if randomChoice {
allAnnotations <- extractAnnotations(ctx)
} else {
allAnnotations <- request.AuditEventFrom(ctx).Annotations
allAnnotations <- audit.AuditEventFrom(ctx).Annotations
}
}()
}

View File

@ -140,7 +140,10 @@ func createAuditEventAndAttachToContext(req *http.Request, policy audit.PolicyRu
return req, nil, nil, fmt.Errorf("failed to complete audit event from request: %v", err)
}
req = req.WithContext(request.WithAuditEvent(ctx, ev))
req = req.WithContext(audit.WithAuditContext(ctx, &audit.AuditContext{
RequestAuditConfig: audit.RequestAuditConfig{},
Event: ev,
}))
return req, ev, omitStages, nil
}

View File

@ -33,6 +33,7 @@ import (
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
"k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/audit/policy"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/endpoints/request"
@ -839,13 +840,15 @@ func TestAuditIDHttpHeader(t *testing.T) {
}
}
func withTestContext(req *http.Request, user user.Info, audit *auditinternal.Event) *http.Request {
func withTestContext(req *http.Request, user user.Info, ae *auditinternal.Event) *http.Request {
ctx := req.Context()
if user != nil {
ctx = request.WithUser(ctx, user)
}
if audit != nil {
ctx = request.WithAuditEvent(ctx, audit)
if ae != nil {
ctx = audit.WithAuditContext(ctx, &audit.AuditContext{
Event: ae,
})
}
if info, err := newTestRequestInfoResolver().NewRequestInfo(req); err == nil {
ctx = request.WithRequestInfo(ctx, info)

View File

@ -49,7 +49,7 @@ func WithAuthorization(handler http.Handler, a authorizer.Authorizer, s runtime.
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
ae := request.AuditEventFrom(ctx)
ae := audit.AuditEventFrom(ctx)
attributes, err := GetAuthorizerAttributes(ctx)
if err != nil {

View File

@ -166,7 +166,7 @@ func WithImpersonation(handler http.Handler, a authorizer.Authorizer, s runtime.
oldUser, _ := request.UserFrom(ctx)
httplog.LogOf(req, w).Addf("%v is acting as %v", oldUser, newUser)
ae := request.AuditEventFrom(ctx)
ae := audit.AuditEventFrom(ctx)
audit.LogImpersonatedUser(ae, newUser)
// clear all the impersonation headers from the request

View File

@ -33,6 +33,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
"k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/audit/policy"
"k8s.io/apiserver/pkg/endpoints/request"
testingclock "k8s.io/utils/clock/testing"
@ -410,7 +411,7 @@ func TestWithFailedRequestAudit(t *testing.T) {
t.Errorf("expected an http.ResponseWriter of type: %T but got: %T", &auditResponseWriter{}, rwGot)
}
auditEventGot := request.AuditEventFrom(requestGot.Context())
auditEventGot := audit.AuditEventFrom(requestGot.Context())
if auditEventGot == nil {
t.Fatal("expected an audit event object but got nil")
}

View File

@ -141,7 +141,7 @@ func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Int
}
ctx = request.WithNamespace(ctx, namespace)
ae := request.AuditEventFrom(ctx)
ae := audit.AuditEventFrom(ctx)
admit = admission.WithAudit(admit, ae)
audit.LogRequestObject(ae, obj, objGV, scope.Resource, scope.Subresource, scope.Serializer)

View File

@ -67,7 +67,7 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope *RequestSc
defer cancel()
ctx = request.WithNamespace(ctx, namespace)
ae := request.AuditEventFrom(ctx)
ae := audit.AuditEventFrom(ctx)
admit = admission.WithAudit(admit, ae)
outputMediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, scope)
@ -103,7 +103,7 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope *RequestSc
}
trace.Step("Decoded delete options")
ae := request.AuditEventFrom(ctx)
ae := audit.AuditEventFrom(ctx)
objGV := gvk.GroupVersion()
audit.LogRequestObject(ae, obj, objGV, scope.Resource, scope.Subresource, scope.Serializer)
trace.Step("Recorded the audit event")
@ -189,7 +189,7 @@ func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope *RequestSc
defer cancel()
ctx = request.WithNamespace(ctx, namespace)
ae := request.AuditEventFrom(ctx)
ae := audit.AuditEventFrom(ctx)
outputMediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, scope)
if err != nil {
@ -250,7 +250,7 @@ func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope *RequestSc
return
}
ae := request.AuditEventFrom(ctx)
ae := audit.AuditEventFrom(ctx)
objGV := gvk.GroupVersion()
audit.LogRequestObject(ae, obj, objGV, scope.Resource, scope.Subresource, scope.Serializer)
} else {

View File

@ -123,7 +123,7 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac
}
options.TypeMeta.SetGroupVersionKind(metav1.SchemeGroupVersion.WithKind("PatchOptions"))
ae := request.AuditEventFrom(ctx)
ae := audit.AuditEventFrom(ctx)
admit = admission.WithAudit(admit, ae)
audit.LogRequestPatch(ae, patchBytes)

View File

@ -267,7 +267,7 @@ func WriteObjectNegotiated(s runtime.NegotiatedSerializer, restrictions negotiat
return
}
if ae := request.AuditEventFrom(req.Context()); ae != nil {
if ae := audit.AuditEventFrom(req.Context()); ae != nil {
audit.LogResponseObject(ae, object, gv, s)
}

View File

@ -39,6 +39,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
@ -187,7 +188,7 @@ func ConnectResource(connecter rest.Connecter, scope *RequestScope, admit admiss
}
ctx := req.Context()
ctx = request.WithNamespace(ctx, namespace)
ae := request.AuditEventFrom(ctx)
ae := audit.AuditEventFrom(ctx)
admit = admission.WithAudit(admit, ae)
opts, subpath, subpathKey := connecter.NewConnectOptions()

View File

@ -118,7 +118,7 @@ func UpdateResource(r rest.Updater, scope *RequestScope, admit admission.Interfa
}
trace.Step("Conversion done")
ae := request.AuditEventFrom(ctx)
ae := audit.AuditEventFrom(ctx)
audit.LogRequestObject(ae, obj, objGV, scope.Resource, scope.Subresource, scope.Serializer)
admit = admission.WithAudit(admit, ae)

View File

@ -20,7 +20,6 @@ import (
"context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/apis/audit"
"k8s.io/apiserver/pkg/authentication/user"
)
@ -33,9 +32,6 @@ const (
// userKey is the context key for the request user.
userKey
// auditKey is the context key for the audit event.
auditKey
)
// NewContext instantiates a base context object for request flows.
@ -80,14 +76,3 @@ func UserFrom(ctx context.Context) (user.Info, bool) {
user, ok := ctx.Value(userKey).(user.Info)
return user, ok
}
// WithAuditEvent returns set audit event struct.
func WithAuditEvent(parent context.Context, ev *audit.Event) context.Context {
return WithValue(parent, auditKey, ev)
}
// AuditEventFrom returns the audit event struct on the ctx
func AuditEventFrom(ctx context.Context) *audit.Event {
ev, _ := ctx.Value(auditKey).(*audit.Event)
return ev
}

View File

@ -281,7 +281,7 @@ func TestAuthenticationAuditAnnotationsDefaultChain(t *testing.T) {
audit.AddAuditAnnotation(req.Context(), "pandas", "are awesome")
// confirm that trying to use the audit event directly would never work
if ae := request.AuditEventFrom(req.Context()); ae != nil {
if ae := audit.AuditEventFrom(req.Context()); ae != nil {
t.Errorf("expected nil audit event, got %v", ae)
}
@ -307,7 +307,7 @@ func TestAuthenticationAuditAnnotationsDefaultChain(t *testing.T) {
}
// confirm that we have an audit event
ae := request.AuditEventFrom(r.Context())
ae := audit.AuditEventFrom(r.Context())
if ae == nil {
t.Error("unexpected nil audit event")
}