Skip to content

Commit 0bc6211

Browse files
committed
Allow handlers early in the request chain to set audit annotations
This change adds the generic ability for request handlers that run before WithAudit to set annotations in the audit.Event.Annotations map. Note that this change does not use this capability yet. Determining which handlers should set audit annotations and what keys and values should be used requires further discussion (this data will become part of our public API). Signed-off-by: Monis Khan <[email protected]>
1 parent ede025a commit 0bc6211

File tree

8 files changed

+229
-0
lines changed

8 files changed

+229
-0
lines changed

staging/src/k8s.io/apiserver/pkg/audit/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ load(
99
go_library(
1010
name = "go_default_library",
1111
srcs = [
12+
"context.go",
1213
"format.go",
1314
"metrics.go",
1415
"request.go",
@@ -35,6 +36,7 @@ go_library(
3536
"//staging/src/k8s.io/apiserver/pkg/apis/audit/v1beta1:go_default_library",
3637
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
3738
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
39+
"//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
3840
"//staging/src/k8s.io/component-base/metrics:go_default_library",
3941
"//staging/src/k8s.io/component-base/metrics/legacyregistry:go_default_library",
4042
"//vendor/github.com/google/uuid:go_default_library",
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package audit
18+
19+
import (
20+
"context"
21+
22+
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
23+
)
24+
25+
// The key type is unexported to prevent collisions
26+
type key int
27+
28+
const (
29+
// auditAnnotationsKey is the context key for the audit annotations.
30+
auditAnnotationsKey key = iota
31+
)
32+
33+
// annotations = *[]annotation instead of a map to preserve order of insertions
34+
type annotation struct {
35+
key, value string
36+
}
37+
38+
// WithAuditAnnotations returns a new context that can store audit annotations
39+
// via the AddAuditAnnotation function. This function is meant to be called from
40+
// an early request handler to allow all later layers to set audit annotations.
41+
// This is required to support flows where handlers that come before WithAudit
42+
// (such as WithAuthentication) wish to set audit annotations.
43+
func WithAuditAnnotations(parent context.Context) context.Context {
44+
// this should never really happen, but prevent double registration of this slice
45+
if _, ok := parent.Value(auditAnnotationsKey).(*[]annotation); ok {
46+
return parent
47+
}
48+
49+
var annotations []annotation // avoid allocations until we actually need it
50+
return genericapirequest.WithValue(parent, auditAnnotationsKey, &annotations)
51+
}
52+
53+
// AddAuditAnnotation sets the audit annotation for the given key, value pair.
54+
// It is safe to call at most parts of request flow that come after WithAuditAnnotations.
55+
// The notable exception being that this function must not be called via a
56+
// defer statement (i.e. after ServeHTTP) in a handler that runs before WithAudit
57+
// as at that point the audit event has already been sent to the audit sink.
58+
// Handlers that are unaware of their position in the overall request flow should
59+
// prefer AddAuditAnnotation over LogAnnotation to avoid dropping annotations.
60+
func AddAuditAnnotation(ctx context.Context, key, value string) {
61+
// use the audit event directly if we have it
62+
if ae := genericapirequest.AuditEventFrom(ctx); ae != nil {
63+
LogAnnotation(ae, key, value)
64+
return
65+
}
66+
67+
annotations, ok := ctx.Value(auditAnnotationsKey).(*[]annotation)
68+
if !ok {
69+
return // adding audit annotation is not supported at this call site
70+
}
71+
72+
*annotations = append(*annotations, annotation{key: key, value: value})
73+
}
74+
75+
// This is private to prevent reads/write to the slice from outside of this package.
76+
// The audit event should be directly read to get access to the annotations.
77+
func auditAnnotationsFrom(ctx context.Context) []annotation {
78+
annotations, ok := ctx.Value(auditAnnotationsKey).(*[]annotation)
79+
if !ok {
80+
return nil // adding audit annotation is not supported at this call site
81+
}
82+
83+
return *annotations
84+
}

staging/src/k8s.io/apiserver/pkg/audit/request.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ func NewEventFromRequest(req *http.Request, level auditinternal.Level, attribs a
8888
}
8989
}
9090

91+
for _, kv := range auditAnnotationsFrom(req.Context()) {
92+
LogAnnotation(ev, kv.key, kv.value)
93+
}
94+
9195
return ev, nil
9296
}
9397

staging/src/k8s.io/apiserver/pkg/endpoints/filters/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ go_library(
4545
name = "go_default_library",
4646
srcs = [
4747
"audit.go",
48+
"audit_annotations.go",
4849
"authentication.go",
4950
"authn_audit.go",
5051
"authorization.go",
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package filters
18+
19+
import (
20+
"net/http"
21+
22+
"k8s.io/apiserver/pkg/audit"
23+
"k8s.io/apiserver/pkg/audit/policy"
24+
)
25+
26+
// WithAuditAnnotations decorates a http.Handler with a []{key, value} that is merged
27+
// with the audit.Event.Annotations map. This allows layers that run before WithAudit
28+
// (such as authentication) to assert annotations.
29+
// If sink or audit policy is nil, no decoration takes place.
30+
func WithAuditAnnotations(handler http.Handler, sink audit.Sink, policy policy.Checker) http.Handler {
31+
// no need to wrap if auditing is disabled
32+
if sink == nil || policy == nil {
33+
return handler
34+
}
35+
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
36+
req = req.WithContext(audit.WithAuditAnnotations(req.Context()))
37+
handler.ServeHTTP(w, req)
38+
})
39+
}

staging/src/k8s.io/apiserver/pkg/server/BUILD

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,28 @@ go_test(
2424
"//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library",
2525
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
2626
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
27+
"//staging/src/k8s.io/apimachinery/pkg/util/waitgroup:go_default_library",
2728
"//staging/src/k8s.io/apimachinery/pkg/version:go_default_library",
29+
"//staging/src/k8s.io/apiserver/pkg/apis/audit:go_default_library",
2830
"//staging/src/k8s.io/apiserver/pkg/apis/example:go_default_library",
2931
"//staging/src/k8s.io/apiserver/pkg/apis/example/v1:go_default_library",
32+
"//staging/src/k8s.io/apiserver/pkg/audit:go_default_library",
33+
"//staging/src/k8s.io/apiserver/pkg/audit/policy:go_default_library",
34+
"//staging/src/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
35+
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
3036
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
3137
"//staging/src/k8s.io/apiserver/pkg/endpoints/discovery:go_default_library",
3238
"//staging/src/k8s.io/apiserver/pkg/endpoints/filters:go_default_library",
3339
"//staging/src/k8s.io/apiserver/pkg/endpoints/openapi:go_default_library",
40+
"//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
3441
"//staging/src/k8s.io/apiserver/pkg/registry/rest:go_default_library",
3542
"//staging/src/k8s.io/apiserver/pkg/server/filters:go_default_library",
3643
"//staging/src/k8s.io/apiserver/pkg/server/healthz:go_default_library",
3744
"//staging/src/k8s.io/client-go/informers:go_default_library",
3845
"//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library",
3946
"//staging/src/k8s.io/client-go/rest:go_default_library",
4047
"//vendor/github.com/go-openapi/spec:go_default_library",
48+
"//vendor/github.com/google/go-cmp/cmp:go_default_library",
4149
"//vendor/github.com/stretchr/testify/assert:go_default_library",
4250
"//vendor/k8s.io/kube-openapi/pkg/common:go_default_library",
4351
"//vendor/k8s.io/utils/net:go_default_library",

staging/src/k8s.io/apiserver/pkg/server/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,7 @@ func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
676676
if c.SecureServing != nil && !c.SecureServing.DisableHTTP2 && c.GoawayChance > 0 {
677677
handler = genericfilters.WithProbabilisticGoaway(handler, c.GoawayChance)
678678
}
679+
handler = genericapifilters.WithAuditAnnotations(handler, c.AuditBackend, c.AuditPolicyChecker)
679680
handler = genericfilters.WithPanicRecovery(handler)
680681
return handler
681682
}

staging/src/k8s.io/apiserver/pkg/server/config_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,18 @@ import (
2525
"net/http/httputil"
2626
"reflect"
2727
"testing"
28+
"time"
2829

30+
"github.com/google/go-cmp/cmp"
2931
"k8s.io/apimachinery/pkg/util/json"
3032
"k8s.io/apimachinery/pkg/util/sets"
33+
"k8s.io/apimachinery/pkg/util/waitgroup"
34+
auditinternal "k8s.io/apiserver/pkg/apis/audit"
35+
"k8s.io/apiserver/pkg/audit"
36+
"k8s.io/apiserver/pkg/audit/policy"
37+
"k8s.io/apiserver/pkg/authentication/authenticator"
38+
"k8s.io/apiserver/pkg/authentication/user"
39+
"k8s.io/apiserver/pkg/endpoints/request"
3140
"k8s.io/apiserver/pkg/server/healthz"
3241
"k8s.io/client-go/informers"
3342
"k8s.io/client-go/kubernetes/fake"
@@ -241,3 +250,84 @@ func checkExpectedPathsAtRoot(url string, expectedPaths []string, t *testing.T)
241250
}
242251
})
243252
}
253+
254+
func TestAuthenticationAuditAnnotationsDefaultChain(t *testing.T) {
255+
authn := authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) {
256+
// confirm that we can set an audit annotation in a handler before WithAudit
257+
audit.AddAuditAnnotation(req.Context(), "pandas", "are awesome")
258+
259+
// confirm that trying to use the audit event directly would never work
260+
if ae := request.AuditEventFrom(req.Context()); ae != nil {
261+
t.Errorf("expected nil audit event, got %v", ae)
262+
}
263+
264+
return &authenticator.Response{User: &user.DefaultInfo{}}, true, nil
265+
})
266+
backend := &testBackend{}
267+
c := &Config{
268+
Authentication: AuthenticationInfo{Authenticator: authn},
269+
AuditBackend: backend,
270+
AuditPolicyChecker: policy.FakeChecker(auditinternal.LevelMetadata, nil),
271+
272+
// avoid nil panics
273+
HandlerChainWaitGroup: &waitgroup.SafeWaitGroup{},
274+
RequestInfoResolver: &request.RequestInfoFactory{},
275+
RequestTimeout: 10 * time.Second,
276+
LongRunningFunc: func(_ *http.Request, _ *request.RequestInfo) bool { return false },
277+
}
278+
279+
h := DefaultBuildHandlerChain(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
280+
// confirm this is a no-op
281+
if r.Context() != audit.WithAuditAnnotations(r.Context()) {
282+
t.Error("unexpected double wrapping of context")
283+
}
284+
285+
// confirm that we have an audit event
286+
ae := request.AuditEventFrom(r.Context())
287+
if ae == nil {
288+
t.Error("unexpected nil audit event")
289+
}
290+
291+
// confirm that the direct way of setting audit annotations later in the chain works as expected
292+
audit.LogAnnotation(ae, "snorlax", "is cool too")
293+
294+
// confirm that the indirect way of setting audit annotations later in the chain also works
295+
audit.AddAuditAnnotation(r.Context(), "dogs", "are okay")
296+
297+
if _, err := w.Write([]byte("done")); err != nil {
298+
t.Errorf("failed to write response: %v", err)
299+
}
300+
}), c)
301+
w := httptest.NewRecorder()
302+
303+
h.ServeHTTP(w, httptest.NewRequest("GET", "https://ignored.com", nil))
304+
305+
r := w.Result()
306+
if ok := r.StatusCode == http.StatusOK && w.Body.String() == "done" && len(r.Header.Get(auditinternal.HeaderAuditID)) > 0; !ok {
307+
t.Errorf("invalid response: %#v", w)
308+
}
309+
if len(backend.events) == 0 {
310+
t.Error("expected audit events, got none")
311+
}
312+
// these should all be the same because the handler chain mutates the event in place
313+
want := map[string]string{"pandas": "are awesome", "snorlax": "is cool too", "dogs": "are okay"}
314+
for _, event := range backend.events {
315+
if event.Stage != auditinternal.StageResponseComplete {
316+
t.Errorf("expected event stage to be complete, got: %s", event.Stage)
317+
}
318+
if diff := cmp.Diff(want, event.Annotations); diff != "" {
319+
t.Errorf("event has unexpected annotations (-want +got): %s", diff)
320+
}
321+
}
322+
}
323+
324+
type testBackend struct {
325+
events []*auditinternal.Event
326+
327+
audit.Backend // nil panic if anything other than ProcessEvents called
328+
}
329+
330+
func (b *testBackend) ProcessEvents(events ...*auditinternal.Event) bool {
331+
b.events = append(b.events, events...)
332+
return true
333+
}

0 commit comments

Comments
 (0)