Skip to content

Commit a1dc52e

Browse files
authored
Merge pull request kubernetes#89305 from enj/enj/i/authn_audit_annotation
Allow authenticators to set audit annotations
2 parents c894c7b + 0bc6211 commit a1dc52e

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)