diff --git a/staging/src/k8s.io/apiserver/pkg/admission/BUILD b/staging/src/k8s.io/apiserver/pkg/admission/BUILD index 45df48cc6a1..dd4f2564110 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/admission/BUILD @@ -10,6 +10,7 @@ go_test( name = "go_default_test", srcs = [ "attributes_test.go", + "audit_test.go", "chain_test.go", "config_test.go", "errors_test.go", @@ -18,12 +19,14 @@ go_test( embed = [":go_default_library"], deps = [ "//vendor/github.com/stretchr/testify/assert:go_default_library", + "//vendor/github.com/stretchr/testify/require: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/util/json:go_default_library", "//vendor/k8s.io/apiserver/pkg/apis/apiserver:go_default_library", "//vendor/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1:go_default_library", + "//vendor/k8s.io/apiserver/pkg/apis/audit:go_default_library", ], ) @@ -31,6 +34,7 @@ go_library( name = "go_default_library", srcs = [ "attributes.go", + "audit.go", "chain.go", "config.go", "decorator.go", @@ -53,6 +57,8 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/util/validation:go_default_library", "//vendor/k8s.io/apiserver/pkg/apis/apiserver:go_default_library", "//vendor/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1:go_default_library", + "//vendor/k8s.io/apiserver/pkg/apis/audit:go_default_library", + "//vendor/k8s.io/apiserver/pkg/audit:go_default_library", "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", ], ) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/audit.go b/staging/src/k8s.io/apiserver/pkg/admission/audit.go new file mode 100644 index 00000000000..13d86b33b96 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/audit.go @@ -0,0 +1,95 @@ +/* +Copyright 2018 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 admission + +import ( + "fmt" + + auditinternal "k8s.io/apiserver/pkg/apis/audit" + "k8s.io/apiserver/pkg/audit" +) + +// auditHandler logs annotations set by other admission handlers +type auditHandler struct { + Interface + ae *auditinternal.Event +} + +var _ Interface = &auditHandler{} +var _ MutationInterface = &auditHandler{} +var _ ValidationInterface = &auditHandler{} + +// WithAudit is a decorator for a admission phase. It saves annotations +// of attribute into the audit event. Attributes passed to the Admit and +// Validate function must be instance of privateAnnotationsGetter or +// AnnotationsGetter, otherwise an error is returned. +func WithAudit(i Interface, ae *auditinternal.Event) Interface { + if i == nil { + return i + } + return &auditHandler{i, ae} +} + +func (handler auditHandler) Admit(a Attributes) error { + if !handler.Interface.Handles(a.GetOperation()) { + return nil + } + if err := ensureAnnotationGetter(a); err != nil { + return err + } + var err error + if mutator, ok := handler.Interface.(MutationInterface); ok { + err = mutator.Admit(a) + handler.logAnnotations(a) + } + return err +} + +func (handler auditHandler) Validate(a Attributes) error { + if !handler.Interface.Handles(a.GetOperation()) { + return nil + } + if err := ensureAnnotationGetter(a); err != nil { + return err + } + var err error + if validator, ok := handler.Interface.(ValidationInterface); ok { + err = validator.Validate(a) + handler.logAnnotations(a) + } + return err +} + +func ensureAnnotationGetter(a Attributes) error { + _, okPrivate := a.(privateAnnotationsGetter) + _, okPublic := a.(AnnotationsGetter) + if okPrivate || okPublic { + return nil + } + return fmt.Errorf("attributes must be an instance of privateAnnotationsGetter or AnnotationsGetter") +} + +func (handler auditHandler) logAnnotations(a Attributes) { + switch a := a.(type) { + case privateAnnotationsGetter: + audit.LogAnnotations(handler.ae, a.getAnnotations()) + case AnnotationsGetter: + audit.LogAnnotations(handler.ae, a.GetAnnotations()) + default: + // this will never happen, because we have already checked it in ensureAnnotationGetter + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/audit_test.go b/staging/src/k8s.io/apiserver/pkg/admission/audit_test.go new file mode 100644 index 00000000000..31f3b5881cf --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/audit_test.go @@ -0,0 +1,173 @@ +/* +Copyright 2018 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 admission + +import ( + "fmt" + "testing" + + "k8s.io/apimachinery/pkg/runtime/schema" + auditinternal "k8s.io/apiserver/pkg/apis/audit" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// fakeHandler implements Interface +type fakeHandler struct { + // return value of Admit() + admit error + // annotations add to attributesRecord during Admit() phase + admitAnnotations map[string]string + // return value of Validate() + validate error + // annotations add to attributesRecord during Validate() phase + validateAnnotations map[string]string + // return value of Handles() + handles bool +} + +var _ Interface = &fakeHandler{} +var _ MutationInterface = &fakeHandler{} +var _ ValidationInterface = &fakeHandler{} + +func (h fakeHandler) Admit(a Attributes) error { + for k, v := range h.admitAnnotations { + a.AddAnnotation(k, v) + } + return h.admit +} + +func (h fakeHandler) Validate(a Attributes) error { + for k, v := range h.validateAnnotations { + a.AddAnnotation(k, v) + } + return h.validate +} + +func (h fakeHandler) Handles(o Operation) bool { + return h.handles +} + +func attributes() Attributes { + return NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "", "", schema.GroupVersionResource{}, "", "", nil) +} + +func TestWithAudit(t *testing.T) { + var testCases = map[string]struct { + admit error + admitAnnotations map[string]string + validate error + validateAnnotations map[string]string + handles bool + }{ + "not handle": { + nil, + nil, + nil, + nil, + false, + }, + "allow": { + nil, + nil, + nil, + nil, + true, + }, + "allow with annotations": { + nil, + map[string]string{ + "plugin.example.com/foo": "bar", + }, + nil, + nil, + true, + }, + "allow with annotations overwrite": { + nil, + map[string]string{ + "plugin.example.com/foo": "bar", + }, + nil, + map[string]string{ + "plugin.example.com/foo": "bar", + }, + true, + }, + "forbidden error": { + NewForbidden(attributes(), fmt.Errorf("quota exceeded")), + nil, + NewForbidden(attributes(), fmt.Errorf("quota exceeded")), + nil, + true, + }, + "forbidden error with annotations": { + NewForbidden(attributes(), fmt.Errorf("quota exceeded")), + nil, + NewForbidden(attributes(), fmt.Errorf("quota exceeded")), + map[string]string{ + "plugin.example.com/foo": "bar", + }, + true, + }, + "forbidden error with annotations overwrite": { + NewForbidden(attributes(), fmt.Errorf("quota exceeded")), + map[string]string{ + "plugin.example.com/foo": "bar", + }, + NewForbidden(attributes(), fmt.Errorf("quota exceeded")), + map[string]string{ + "plugin.example.com/foo": "bar", + }, + true, + }, + } + for tcName, tc := range testCases { + var handler Interface = fakeHandler{tc.admit, tc.admitAnnotations, tc.validate, tc.validateAnnotations, tc.handles} + ae := &auditinternal.Event{Level: auditinternal.LevelMetadata} + auditHandler := WithAudit(handler, ae) + a := attributes() + + assert.Equal(t, handler.Handles(Create), auditHandler.Handles(Create), tcName+": WithAudit decorator should not effect the return value") + + mutator, ok := handler.(MutationInterface) + require.True(t, ok) + auditMutator, ok := auditHandler.(MutationInterface) + require.True(t, ok) + assert.Equal(t, mutator.Admit(a), auditMutator.Admit(a), tcName+": WithAudit decorator should not effect the return value") + + validator, ok := handler.(ValidationInterface) + require.True(t, ok) + auditValidator, ok := auditHandler.(ValidationInterface) + require.True(t, ok) + assert.Equal(t, validator.Validate(a), auditValidator.Validate(a), tcName+": WithAudit decorator should not effect the return value") + + annotations := make(map[string]string, len(tc.admitAnnotations)+len(tc.validateAnnotations)) + for k, v := range tc.admitAnnotations { + annotations[k] = v + } + for k, v := range tc.validateAnnotations { + annotations[k] = v + } + if len(annotations) == 0 { + assert.Nil(t, ae.Annotations, tcName+": unexptected annotations set in audit event") + } else { + assert.Equal(t, annotations, ae.Annotations, tcName+": unexptected annotations set in audit event") + } + } +}