From c030026b544da2dd7ef7201019bdc0ac255c2d23 Mon Sep 17 00:00:00 2001 From: CaoShufeng Date: Wed, 6 Sep 2017 21:31:14 +0800 Subject: [PATCH] enhance unit tests of advance audit feature This change does three things: 1. use auditinternal for unit test in filter stage 2. add a seperate unit test for Audit-ID http header 3. add unit test for audit log backend --- .../apiserver/pkg/endpoints/filters/BUILD | 4 - .../pkg/endpoints/filters/audit_test.go | 537 +++++------------- .../apiserver/plugin/pkg/audit/log/BUILD | 20 + .../plugin/pkg/audit/log/backend_test.go | 175 ++++++ 4 files changed, 348 insertions(+), 388 deletions(-) create mode 100644 staging/src/k8s.io/apiserver/plugin/pkg/audit/log/backend_test.go diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/BUILD index a1c34ea660..ec1116dae3 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/BUILD @@ -29,16 +29,12 @@ go_test( "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//vendor/k8s.io/apiserver/pkg/apis/audit:go_default_library", - "//vendor/k8s.io/apiserver/pkg/apis/audit/v1beta1:go_default_library", - "//vendor/k8s.io/apiserver/pkg/audit:go_default_library", "//vendor/k8s.io/apiserver/pkg/audit/policy:go_default_library", "//vendor/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library", "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", "//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library", "//vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters:go_default_library", "//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library", - "//vendor/k8s.io/apiserver/plugin/pkg/audit/log:go_default_library", - "//vendor/k8s.io/apiserver/plugin/pkg/audit/webhook: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 bfac84f8fa..fe0317d73b 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 @@ -18,14 +18,10 @@ package filters import ( "bufio" - "bytes" - "fmt" "net" "net/http" "net/http/httptest" "reflect" - "regexp" - "strings" "sync" "testing" "time" @@ -33,18 +29,12 @@ import ( "github.com/pborman/uuid" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" auditinternal "k8s.io/apiserver/pkg/apis/audit" - auditv1beta1 "k8s.io/apiserver/pkg/apis/audit/v1beta1" - "k8s.io/apiserver/pkg/audit" "k8s.io/apiserver/pkg/audit/policy" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/endpoints/request" - pluginlog "k8s.io/apiserver/plugin/pkg/audit/log" - // import to call webhook's init() function to register audit.Event to schema - _ "k8s.io/apiserver/plugin/pkg/audit/webhook" ) type fakeAuditSink struct { @@ -55,7 +45,10 @@ type fakeAuditSink struct { func (s *fakeAuditSink) ProcessEvents(evs ...*auditinternal.Event) { s.lock.Lock() defer s.lock.Unlock() - s.events = append(s.events, evs...) + for _, e := range evs { + event := e.DeepCopy() + s.events = append(s.events, event) + } } func (s *fakeAuditSink) Events() []*auditinternal.Event { @@ -166,7 +159,7 @@ func TestDecorateResponseWriterChannel(t *testing.T) { } t.Logf("Seen event with status %v", ev1.ResponseStatus) - if ev != ev1 { + if !reflect.DeepEqual(ev, ev1) { t.Fatalf("ev1 and ev must be equal") } @@ -186,288 +179,7 @@ func (*fakeHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { w.WriteHeader(200) } -func TestAuditLegacy(t *testing.T) { - writingShortRunningPrefix := func(stage string) string { - return fmt.Sprintf(`[\d\:\-\.\+TZ]+ AUDIT: id="[\w-]+" stage="%s" ip="127.0.0.1" method="update" user="admin" groups="" as="" asgroups="" namespace="default" uri="/api/v1/namespaces/default/pods/foo"`, stage) - } - readOnlyShortRunningPrefix := func(stage string) string { - return fmt.Sprintf(`[\d\:\-\.\+TZ]+ AUDIT: id="[\w-]+" stage="%s" ip="127.0.0.1" method="get" user="admin" groups="" as="" asgroups="" namespace="default" uri="/api/v1/namespaces/default/pods/foo"`, stage) - } - longRunningPrefix := func(stage string) string { - return fmt.Sprintf(`[\d\:\-\.\+TZ]+ AUDIT: id="[\w-]+" stage="%s" ip="127.0.0.1" method="watch" user="admin" groups="" as="" asgroups="" namespace="default" uri="/api/v1/namespaces/default/pods\?watch=true"`, stage) - } - - shortRunningPath := "/api/v1/namespaces/default/pods/foo" - longRunningPath := "/api/v1/namespaces/default/pods?watch=true" - - delay := 500 * time.Millisecond - - for _, test := range []struct { - desc string - path string - verb string - omitStages []auditinternal.Stage - handler func(http.ResponseWriter, *http.Request) - expected []string - }{ - // short running requests with read-only verb - { - "read-only empty", - shortRunningPath, - "GET", - nil, - func(http.ResponseWriter, *http.Request) {}, - []string{ - readOnlyShortRunningPrefix(auditinternal.StageRequestReceived) + ` response=""`, - readOnlyShortRunningPrefix(auditinternal.StageResponseComplete) + ` response="200"`, - }, - }, - { - "read-only panic", - shortRunningPath, - "GET", - nil, - func(w http.ResponseWriter, req *http.Request) { - panic("kaboom") - }, - []string{ - readOnlyShortRunningPrefix(auditinternal.StageRequestReceived) + ` response=""`, - readOnlyShortRunningPrefix(auditinternal.StagePanic) + ` response="500"`, - }, - }, - - // short running request with non-read-only verb - { - "writing empty", - shortRunningPath, - "PUT", - nil, - func(http.ResponseWriter, *http.Request) {}, - []string{ - writingShortRunningPrefix(auditinternal.StageRequestReceived) + ` response=""`, - writingShortRunningPrefix(auditinternal.StageResponseComplete) + ` response="200"`, - }, - }, - { - "writing sleep", - shortRunningPath, - "PUT", - nil, - func(http.ResponseWriter, *http.Request) { - time.Sleep(delay) - }, - []string{ - writingShortRunningPrefix(auditinternal.StageRequestReceived) + ` response=""`, - writingShortRunningPrefix(auditinternal.StageResponseComplete) + ` response="200"`, - }, - }, - { - "writing 403+write", - shortRunningPath, - "PUT", - nil, - func(w http.ResponseWriter, req *http.Request) { - w.WriteHeader(403) - w.Write([]byte("foo")) - }, - []string{ - writingShortRunningPrefix(auditinternal.StageRequestReceived) + ` response=""`, - writingShortRunningPrefix(auditinternal.StageResponseComplete) + ` response="403"`, - }, - }, - { - "writing panic", - shortRunningPath, - "PUT", - nil, - func(w http.ResponseWriter, req *http.Request) { - panic("kaboom") - }, - []string{ - writingShortRunningPrefix(auditinternal.StageRequestReceived) + ` response=""`, - writingShortRunningPrefix(auditinternal.StagePanic) + ` response="500"`, - }, - }, - { - "writing write+panic", - shortRunningPath, - "PUT", - nil, - func(w http.ResponseWriter, req *http.Request) { - w.Write([]byte("foo")) - panic("kaboom") - }, - []string{ - writingShortRunningPrefix(auditinternal.StageRequestReceived) + ` response=""`, - writingShortRunningPrefix(auditinternal.StagePanic) + ` response="500"`, - }, - }, - - // long running requests - { - "empty longrunning", - longRunningPath, - "GET", - nil, - func(http.ResponseWriter, *http.Request) {}, - []string{ - longRunningPrefix(auditinternal.StageRequestReceived) + ` response=""`, - longRunningPrefix(auditinternal.StageResponseStarted) + ` response="200"`, - longRunningPrefix(auditinternal.StageResponseComplete) + ` response="200"`, - }, - }, - { - "sleep longrunning", - longRunningPath, - "GET", - nil, - func(http.ResponseWriter, *http.Request) { - time.Sleep(delay) - }, - []string{ - longRunningPrefix(auditinternal.StageRequestReceived) + ` response=""`, - longRunningPrefix(auditinternal.StageResponseStarted) + ` response="200"`, - longRunningPrefix(auditinternal.StageResponseComplete) + ` response="200"`, - }, - }, - { - "sleep+403 longrunning", - longRunningPath, - "GET", - nil, - func(w http.ResponseWriter, req *http.Request) { - time.Sleep(delay) - w.WriteHeader(403) - }, - []string{ - longRunningPrefix(auditinternal.StageRequestReceived) + ` response=""`, - longRunningPrefix(auditinternal.StageResponseStarted) + ` response="403"`, - longRunningPrefix(auditinternal.StageResponseComplete) + ` response="403"`, - }, - }, - { - "write longrunning", - longRunningPath, - "GET", - nil, - func(w http.ResponseWriter, req *http.Request) { - w.Write([]byte("foo")) - }, - []string{ - longRunningPrefix(auditinternal.StageRequestReceived) + ` response=""`, - longRunningPrefix(auditinternal.StageResponseStarted) + ` response="200"`, - longRunningPrefix(auditinternal.StageResponseComplete) + ` response="200"`, - }, - }, - { - "403+write longrunning", - longRunningPath, - "GET", - nil, - func(w http.ResponseWriter, req *http.Request) { - w.WriteHeader(403) - w.Write([]byte("foo")) - }, - []string{ - longRunningPrefix(auditinternal.StageRequestReceived) + ` response=""`, - longRunningPrefix(auditinternal.StageResponseStarted) + ` response="403"`, - longRunningPrefix(auditinternal.StageResponseComplete) + ` response="403"`, - }, - }, - { - "panic longrunning", - longRunningPath, - "GET", - nil, - func(w http.ResponseWriter, req *http.Request) { - panic("kaboom") - }, - []string{ - longRunningPrefix(auditinternal.StageRequestReceived) + ` response=""`, - longRunningPrefix(auditinternal.StagePanic) + ` response="500"`, - }, - }, - { - "write+panic longrunning", - longRunningPath, - "GET", - nil, - func(w http.ResponseWriter, req *http.Request) { - w.Write([]byte("foo")) - panic("kaboom") - }, - []string{ - longRunningPrefix(auditinternal.StageRequestReceived) + ` response=""`, - longRunningPrefix(auditinternal.StageResponseStarted) + ` response="200"`, - longRunningPrefix(auditinternal.StagePanic) + ` response="500"`, - }, - }, - { - "omit RequestReceived", - shortRunningPath, - "GET", - []auditinternal.Stage{auditinternal.StageRequestReceived}, - func(http.ResponseWriter, *http.Request) {}, - []string{ - readOnlyShortRunningPrefix(auditinternal.StageResponseComplete) + ` response="200"`, - }, - }, - { - "emit painc only", - longRunningPath, - "GET", - []auditinternal.Stage{auditinternal.StageRequestReceived, auditinternal.StageResponseStarted, auditinternal.StageResponseComplete}, - func(w http.ResponseWriter, req *http.Request) { - w.Write([]byte("foo")) - panic("kaboom") - }, - []string{ - longRunningPrefix(auditinternal.StagePanic) + ` response="500"`, - }, - }, - } { - var buf bytes.Buffer - backend := pluginlog.NewBackend(&buf, pluginlog.FormatLegacy, auditv1beta1.SchemeGroupVersion) - policyChecker := policy.FakeChecker(auditinternal.LevelRequestResponse, test.omitStages) - handler := WithAudit(http.HandlerFunc(test.handler), &fakeRequestContextMapper{ - user: &user.DefaultInfo{Name: "admin"}, - }, backend, policyChecker, func(r *http.Request, ri *request.RequestInfo) bool { - // simplified long-running check - return ri.Verb == "watch" - }) - - req, _ := http.NewRequest(test.verb, test.path, nil) - req.RemoteAddr = "127.0.0.1" - - func() { - defer func() { - recover() - }() - handler.ServeHTTP(httptest.NewRecorder(), req) - }() - - t.Logf("[%s] audit log: %v", test.desc, buf.String()) - - line := strings.Split(strings.TrimSpace(buf.String()), "\n") - if len(line) != len(test.expected) { - t.Errorf("[%s] Unexpected amount of lines in audit log: %d", test.desc, len(line)) - continue - } - - for i, re := range test.expected { - match, err := regexp.MatchString(re, line[i]) - if err != nil { - t.Errorf("[%s] Unexpected error matching line %d: %v", test.desc, i, err) - continue - } - if !match { - t.Errorf("[%s] Unexpected line %d of audit: %s", test.desc, i, line[i]) - } - } - } -} - -func TestAuditJson(t *testing.T) { +func TestAudit(t *testing.T) { shortRunningPath := "/api/v1/namespaces/default/pods/foo" longRunningPath := "/api/v1/namespaces/default/pods?watch=true" @@ -480,7 +192,7 @@ func TestAuditJson(t *testing.T) { auditID string omitStages []auditinternal.Stage handler func(http.ResponseWriter, *http.Request) - expected []auditv1beta1.Event + expected []auditinternal.Event respHeader bool }{ // short running requests with read-only verb @@ -491,7 +203,7 @@ func TestAuditJson(t *testing.T) { "", nil, func(http.ResponseWriter, *http.Request) {}, - []auditv1beta1.Event{ + []auditinternal.Event{ { Stage: auditinternal.StageRequestReceived, Verb: "get", @@ -515,7 +227,7 @@ func TestAuditJson(t *testing.T) { func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("foo")) }, - []auditv1beta1.Event{ + []auditinternal.Event{ { Stage: auditinternal.StageRequestReceived, Verb: "get", @@ -539,7 +251,7 @@ func TestAuditJson(t *testing.T) { func(w http.ResponseWriter, req *http.Request) { panic("kaboom") }, - []auditv1beta1.Event{ + []auditinternal.Event{ { Stage: auditinternal.StageRequestReceived, Verb: "get", @@ -562,7 +274,7 @@ func TestAuditJson(t *testing.T) { "", nil, func(http.ResponseWriter, *http.Request) {}, - []auditv1beta1.Event{ + []auditinternal.Event{ { Stage: auditinternal.StageRequestReceived, Verb: "update", @@ -587,7 +299,7 @@ func TestAuditJson(t *testing.T) { w.Write([]byte("foo")) time.Sleep(delay) }, - []auditv1beta1.Event{ + []auditinternal.Event{ { Stage: auditinternal.StageRequestReceived, Verb: "update", @@ -612,7 +324,7 @@ func TestAuditJson(t *testing.T) { w.WriteHeader(403) w.Write([]byte("foo")) }, - []auditv1beta1.Event{ + []auditinternal.Event{ { Stage: auditinternal.StageRequestReceived, Verb: "update", @@ -636,7 +348,7 @@ func TestAuditJson(t *testing.T) { func(w http.ResponseWriter, req *http.Request) { panic("kaboom") }, - []auditv1beta1.Event{ + []auditinternal.Event{ { Stage: auditinternal.StageRequestReceived, Verb: "update", @@ -661,7 +373,7 @@ func TestAuditJson(t *testing.T) { w.Write([]byte("foo")) panic("kaboom") }, - []auditv1beta1.Event{ + []auditinternal.Event{ { Stage: auditinternal.StageRequestReceived, Verb: "update", @@ -684,7 +396,7 @@ func TestAuditJson(t *testing.T) { "", nil, func(http.ResponseWriter, *http.Request) {}, - []auditv1beta1.Event{ + []auditinternal.Event{ { Stage: auditinternal.StageRequestReceived, Verb: "watch", @@ -714,7 +426,7 @@ func TestAuditJson(t *testing.T) { func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("foo")) }, - []auditv1beta1.Event{ + []auditinternal.Event{ { Stage: auditinternal.StageRequestReceived, Verb: "watch", @@ -744,7 +456,7 @@ func TestAuditJson(t *testing.T) { func(http.ResponseWriter, *http.Request) { time.Sleep(delay) }, - []auditv1beta1.Event{ + []auditinternal.Event{ { Stage: auditinternal.StageRequestReceived, Verb: "watch", @@ -775,7 +487,7 @@ func TestAuditJson(t *testing.T) { time.Sleep(delay) w.WriteHeader(403) }, - []auditv1beta1.Event{ + []auditinternal.Event{ { Stage: auditinternal.StageRequestReceived, Verb: "watch", @@ -805,7 +517,7 @@ func TestAuditJson(t *testing.T) { func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("foo")) }, - []auditv1beta1.Event{ + []auditinternal.Event{ { Stage: auditinternal.StageRequestReceived, Verb: "watch", @@ -836,7 +548,7 @@ func TestAuditJson(t *testing.T) { w.WriteHeader(403) w.Write([]byte("foo")) }, - []auditv1beta1.Event{ + []auditinternal.Event{ { Stage: auditinternal.StageRequestReceived, Verb: "watch", @@ -866,7 +578,7 @@ func TestAuditJson(t *testing.T) { func(w http.ResponseWriter, req *http.Request) { panic("kaboom") }, - []auditv1beta1.Event{ + []auditinternal.Event{ { Stage: auditinternal.StageRequestReceived, Verb: "watch", @@ -891,7 +603,7 @@ func TestAuditJson(t *testing.T) { w.Write([]byte("foo")) panic("kaboom") }, - []auditv1beta1.Event{ + []auditinternal.Event{ { Stage: auditinternal.StageRequestReceived, Verb: "watch", @@ -921,7 +633,7 @@ func TestAuditJson(t *testing.T) { func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("foo")) }, - []auditv1beta1.Event{ + []auditinternal.Event{ { Stage: auditinternal.StageResponseComplete, Verb: "get", @@ -941,7 +653,7 @@ func TestAuditJson(t *testing.T) { w.Write([]byte("foo")) panic("kaboom") }, - []auditv1beta1.Event{ + []auditinternal.Event{ { Stage: auditinternal.StagePanic, Verb: "watch", @@ -952,85 +664,74 @@ func TestAuditJson(t *testing.T) { true, }, } { - var buf bytes.Buffer - backend := pluginlog.NewBackend(&buf, pluginlog.FormatJson, auditv1beta1.SchemeGroupVersion) - policyChecker := policy.FakeChecker(auditinternal.LevelRequestResponse, test.omitStages) - handler := WithAudit(http.HandlerFunc(test.handler), &fakeRequestContextMapper{ - user: &user.DefaultInfo{Name: "admin"}, - }, backend, policyChecker, func(r *http.Request, ri *request.RequestInfo) bool { - // simplified long-running check - return ri.Verb == "watch" - }) + t.Run(test.desc, func(t *testing.T) { + sink := &fakeAuditSink{} + policyChecker := policy.FakeChecker(auditinternal.LevelRequestResponse, test.omitStages) + handler := WithAudit(http.HandlerFunc(test.handler), &fakeRequestContextMapper{ + user: &user.DefaultInfo{Name: "admin"}, + }, sink, policyChecker, func(r *http.Request, ri *request.RequestInfo) bool { + // simplified long-running check + return ri.Verb == "watch" + }) - req, _ := http.NewRequest(test.verb, test.path, nil) - if test.auditID != "" { - req.Header.Add("Audit-ID", test.auditID) - } - req.RemoteAddr = "127.0.0.1" + req, _ := http.NewRequest(test.verb, test.path, nil) + if test.auditID != "" { + req.Header.Add("Audit-ID", test.auditID) + } + req.RemoteAddr = "127.0.0.1" - w := httptest.NewRecorder() - func() { - defer func() { - recover() + func() { + defer func() { + recover() + }() + handler.ServeHTTP(httptest.NewRecorder(), req) }() - handler.ServeHTTP(w, req) - }() - t.Logf("[%s] audit log: %v", test.desc, buf.String()) + events := sink.Events() + t.Logf("audit log: %v", events) - line := strings.Split(strings.TrimSpace(buf.String()), "\n") - if len(line) != len(test.expected) { - t.Errorf("[%s] Unexpected amount of lines in audit log: %d", test.desc, len(line)) - continue - } - expectedID := types.UID("") - for i, expect := range test.expected { - // decode events back to check json elements. - event := &auditv1beta1.Event{} - decoder := audit.Codecs.UniversalDecoder(auditv1beta1.SchemeGroupVersion) - if err := runtime.DecodeInto(decoder, []byte(line[i]), event); err != nil { - t.Errorf("failed decoding line %s: %v", line[i], err) - continue - } - if "admin" != event.User.Username { - t.Errorf("[%s] Unexpected username: %s", test.desc, event.User.Username) - } - if event.Stage != expect.Stage { - t.Errorf("[%s] Unexpected Stage: %s", test.desc, event.Stage) - } - if event.Verb != expect.Verb { - t.Errorf("[%s] Unexpected Verb: %s", test.desc, event.Verb) - } - if event.RequestURI != expect.RequestURI { - t.Errorf("[%s] Unexpected RequestURI: %s", test.desc, event.RequestURI) - } - resp := w.Result() - if test.respHeader && string(event.AuditID) != resp.Header.Get("Audit-Id") { - t.Errorf("[%s] Unexpected Audit-Id http response header, Audit-Id http response header should be the same with AuditID in log %v xx %v", test.desc, event.AuditID, w.HeaderMap.Get("Audit-Id")) + if len(events) != len(test.expected) { + t.Fatalf("Unexpected amount of lines in audit log: %d", len(events)) } + expectedID := types.UID("") + for i, expect := range test.expected { + event := events[i] + if "admin" != event.User.Username { + t.Errorf("Unexpected username: %s", event.User.Username) + } + if event.Stage != expect.Stage { + t.Errorf("Unexpected Stage: %s", event.Stage) + } + if event.Verb != expect.Verb { + t.Errorf("Unexpected Verb: %s", event.Verb) + } + if event.RequestURI != expect.RequestURI { + t.Errorf("Unexpected RequestURI: %s", event.RequestURI) + } - if test.auditID != "" && event.AuditID != types.UID(test.auditID) { - t.Errorf("[%s] Unexpected AuditID in audit event, AuditID should be the same with Audit-ID http header", test.desc) + if test.auditID != "" && event.AuditID != types.UID(test.auditID) { + t.Errorf("Unexpected AuditID in audit event, AuditID should be the same with Audit-ID http header") + } + if expectedID == types.UID("") { + expectedID = event.AuditID + } else if expectedID != event.AuditID { + t.Errorf("Audits for one request should share the same AuditID, %s differs from %s", expectedID, event.AuditID) + } + if event.ObjectRef.APIVersion != "v1" { + t.Errorf("Unexpected apiVersion: %s", event.ObjectRef.APIVersion) + } + if event.ObjectRef.APIGroup != "" { + t.Errorf("Unexpected apiGroup: %s", event.ObjectRef.APIGroup) + } + if (event.ResponseStatus == nil) != (expect.ResponseStatus == nil) { + t.Errorf("Unexpected ResponseStatus: %v", event.ResponseStatus) + continue + } + if (event.ResponseStatus != nil) && (event.ResponseStatus.Code != expect.ResponseStatus.Code) { + t.Errorf("Unexpected status code : %d", event.ResponseStatus.Code) + } } - if expectedID == types.UID("") { - expectedID = event.AuditID - } else if expectedID != event.AuditID { - t.Errorf("[%s] Audits for one request should share the same AuditID, %s differs from %s", test.desc, expectedID, event.AuditID) - } - if event.ObjectRef.APIVersion != "v1" { - t.Errorf("[%s] Unexpected apiVersion: %s", test.desc, event.ObjectRef.APIVersion) - } - if event.ObjectRef.APIGroup != "" { - t.Errorf("[%s] Unexpected apiGroup: %s", test.desc, event.ObjectRef.APIGroup) - } - if (event.ResponseStatus == nil) != (expect.ResponseStatus == nil) { - t.Errorf("[%s] Unexpected ResponseStatus: %v", test.desc, event.ResponseStatus) - continue - } - if (event.ResponseStatus != nil) && (event.ResponseStatus.Code != expect.ResponseStatus.Code) { - t.Errorf("[%s] Unexpected status code : %d", test.desc, event.ResponseStatus.Code) - } - } + }) } } @@ -1084,3 +785,71 @@ func TestAuditLevelNone(t *testing.T) { t.Errorf("Generated events, but should not have: %#v", sink.events) } } + +func TestAuditIDHttpHeader(t *testing.T) { + for _, test := range []struct { + desc string + requestHeader string + level auditinternal.Level + expectedHeader bool + }{ + { + "no http header when there is no audit", + "", + auditinternal.LevelNone, + false, + }, + { + "no http header when there is no audit even the request header specified", + uuid.NewRandom().String(), + auditinternal.LevelNone, + false, + }, + { + "server generated header", + "", + auditinternal.LevelRequestResponse, + true, + }, + { + "user provided header", + uuid.NewRandom().String(), + auditinternal.LevelRequestResponse, + true, + }, + } { + sink := &fakeAuditSink{} + var handler http.Handler + handler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(200) + }) + policyChecker := policy.FakeChecker(test.level, nil) + handler = WithAudit(handler, &fakeRequestContextMapper{ + user: &user.DefaultInfo{Name: "admin"}, + }, sink, policyChecker, nil) + + req, _ := http.NewRequest("GET", "/api/v1/namespaces/default/pods", nil) + req.RemoteAddr = "127.0.0.1" + if test.requestHeader != "" { + req.Header.Add("Audit-ID", test.requestHeader) + } + + w := httptest.NewRecorder() + handler.ServeHTTP(w, req) + resp := w.Result() + if test.expectedHeader { + if resp.Header.Get("Audit-ID") == "" { + t.Errorf("[%s] expected Audit-ID http header returned, but not returned", test.desc) + continue + } + // if get Audit-ID returned, it should be the same same with the requested one + if test.requestHeader != "" && resp.Header.Get("Audit-ID") != test.requestHeader { + t.Errorf("[%s] returned audit http header is not the same with the requested http header, expected: %s, get %s", test.desc, test.requestHeader, resp.Header.Get("Audit-ID")) + } + } else { + if resp.Header.Get("Audit-ID") != "" { + t.Errorf("[%s] expected no Audit-ID http header returned, but got %p", test.desc, resp.Header.Get("Audit-ID")) + } + } + } +} diff --git a/staging/src/k8s.io/apiserver/plugin/pkg/audit/log/BUILD b/staging/src/k8s.io/apiserver/plugin/pkg/audit/log/BUILD index 32f0896dbe..5544c3eed9 100644 --- a/staging/src/k8s.io/apiserver/plugin/pkg/audit/log/BUILD +++ b/staging/src/k8s.io/apiserver/plugin/pkg/audit/log/BUILD @@ -3,6 +3,7 @@ package(default_visibility = ["//visibility:public"]) load( "@io_bazel_rules_go//go:def.bzl", "go_library", + "go_test", ) go_library( @@ -28,3 +29,22 @@ filegroup( srcs = [":package-srcs"], tags = ["automanaged"], ) + +go_test( + name = "go_default_test", + srcs = ["backend_test.go"], + library = ":go_default_library", + deps = [ + "//vendor/github.com/pborman/uuid:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apimachinery/announced:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apimachinery/registered:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", + "//vendor/k8s.io/apiserver/pkg/apis/audit:go_default_library", + "//vendor/k8s.io/apiserver/pkg/apis/audit/install:go_default_library", + "//vendor/k8s.io/apiserver/pkg/apis/audit/v1beta1:go_default_library", + "//vendor/k8s.io/apiserver/pkg/audit:go_default_library", + ], +) diff --git a/staging/src/k8s.io/apiserver/plugin/pkg/audit/log/backend_test.go b/staging/src/k8s.io/apiserver/plugin/pkg/audit/log/backend_test.go new file mode 100644 index 0000000000..84a3f06611 --- /dev/null +++ b/staging/src/k8s.io/apiserver/plugin/pkg/audit/log/backend_test.go @@ -0,0 +1,175 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package log + +import ( + "bytes" + "fmt" + "reflect" + "regexp" + "testing" + "time" + + "github.com/pborman/uuid" + + "k8s.io/apimachinery/pkg/apimachinery/announced" + "k8s.io/apimachinery/pkg/apimachinery/registered" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + auditinternal "k8s.io/apiserver/pkg/apis/audit" + "k8s.io/apiserver/pkg/apis/audit/install" + auditv1beta1 "k8s.io/apiserver/pkg/apis/audit/v1beta1" + "k8s.io/apiserver/pkg/audit" +) + +// NOTE: Copied from webhook backend to register auditv1beta1 to scheme +var ( + groupFactoryRegistry = make(announced.APIGroupFactoryRegistry) + registry = registered.NewOrDie("") +) + +func init() { + allGVs := []schema.GroupVersion{auditv1beta1.SchemeGroupVersion} + registry.RegisterVersions(allGVs) + if err := registry.EnableVersions(allGVs...); err != nil { + panic(fmt.Sprintf("failed to enable version %v", allGVs)) + } + install.Install(groupFactoryRegistry, registry, audit.Scheme) +} + +func TestLogEventsLegacy(t *testing.T) { + for _, test := range []struct { + event *auditinternal.Event + expected string + }{ + { + &auditinternal.Event{ + AuditID: types.UID(uuid.NewRandom().String()), + }, + `[\d\:\-\.\+TZ]+ AUDIT: id="[\w-]+" stage="" ip="" method="" user="" groups="" as="" asgroups="" namespace="" uri="" response=""`, + }, + { + &auditinternal.Event{ + ResponseStatus: &metav1.Status{ + Code: 200, + }, + RequestURI: "/apis/rbac.authorization.k8s.io/v1/roles", + SourceIPs: []string{ + "127.0.0.1", + }, + Timestamp: metav1.NewTime(time.Now()), + AuditID: types.UID(uuid.NewRandom().String()), + Stage: auditinternal.StageRequestReceived, + Verb: "get", + User: auditinternal.UserInfo{ + Username: "admin", + Groups: []string{ + "system:masters", + "system:authenticated", + }, + }, + ObjectRef: &auditinternal.ObjectReference{ + Namespace: "default", + }, + }, + `[\d\:\-\.\+TZ]+ AUDIT: id="[\w-]+" stage="RequestReceived" ip="127.0.0.1" method="get" user="admin" groups="\\"system:masters\\",\\"system:authenticated\\"" as="" asgroups="" namespace="default" uri="/apis/rbac.authorization.k8s.io/v1/roles" response="200"`, + }, + { + &auditinternal.Event{ + AuditID: types.UID(uuid.NewRandom().String()), + Level: auditinternal.LevelMetadata, + ObjectRef: &auditinternal.ObjectReference{ + Resource: "foo", + APIVersion: "v1", + Subresource: "bar", + }, + }, + `[\d\:\-\.\+TZ]+ AUDIT: id="[\w-]+" stage="" ip="" method="" user="" groups="" as="" asgroups="" namespace="" uri="" response=""`, + }, + } { + var buf bytes.Buffer + backend := NewBackend(&buf, FormatLegacy, auditv1beta1.SchemeGroupVersion) + backend.ProcessEvents(test.event) + match, err := regexp.MatchString(test.expected, buf.String()) + if err != nil { + t.Errorf("Unexpected error matching line %v", err) + continue + } + if !match { + t.Errorf("Unexpected line of audit: %s", buf.String()) + } + } +} + +func TestLogEventsJson(t *testing.T) { + for _, event := range []*auditinternal.Event{ + { + AuditID: types.UID(uuid.NewRandom().String()), + }, + { + ResponseStatus: &metav1.Status{ + Code: 200, + }, + RequestURI: "/apis/rbac.authorization.k8s.io/v1/roles", + SourceIPs: []string{ + "127.0.0.1", + }, + // When encoding to json format, the nanosecond part of timestamp is + // lost and it will become zero when we decode event back, so we rounding + // timestamp down to a multiple of second. + Timestamp: metav1.NewTime(time.Now().Truncate(time.Second)), + AuditID: types.UID(uuid.NewRandom().String()), + Stage: auditinternal.StageRequestReceived, + Verb: "get", + User: auditinternal.UserInfo{ + Username: "admin", + Groups: []string{ + "system:masters", + "system:authenticated", + }, + }, + ObjectRef: &auditinternal.ObjectReference{ + Namespace: "default", + }, + }, + { + AuditID: types.UID(uuid.NewRandom().String()), + Level: auditinternal.LevelMetadata, + ObjectRef: &auditinternal.ObjectReference{ + Resource: "foo", + APIVersion: "v1", + Subresource: "bar", + }, + }, + } { + var buf bytes.Buffer + backend := NewBackend(&buf, FormatJson, auditv1beta1.SchemeGroupVersion) + backend.ProcessEvents(event) + // decode events back and compare with the original one. + result := &auditinternal.Event{} + decoder := audit.Codecs.UniversalDecoder(auditv1beta1.SchemeGroupVersion) + if err := runtime.DecodeInto(decoder, buf.Bytes(), result); err != nil { + t.Errorf("failed decoding buf: %s", buf.String()) + continue + } + if !reflect.DeepEqual(event, result) { + t.Errorf("The result event should be the same with the original one, \noriginal: \n%#v\n result: \n%#v", event, result) + } + } +}