audit & admission: associate annotation with audit level

This commit is contained in:
Haowei Cai 2019-05-31 15:36:29 -07:00
parent 82a981fc39
commit 318226f340
7 changed files with 53 additions and 29 deletions

View File

@ -24,6 +24,7 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
) )
@ -42,12 +43,17 @@ type attributesRecord struct {
// other elements are always accessed in single goroutine. // other elements are always accessed in single goroutine.
// But ValidatingAdmissionWebhook add annotations concurrently. // But ValidatingAdmissionWebhook add annotations concurrently.
annotations map[string]string annotations map[string]annotation
annotationsLock sync.RWMutex annotationsLock sync.RWMutex
reinvocationContext ReinvocationContext reinvocationContext ReinvocationContext
} }
type annotation struct {
level auditinternal.Level
value string
}
func NewAttributesRecord(object runtime.Object, oldObject runtime.Object, kind schema.GroupVersionKind, namespace, name string, resource schema.GroupVersionResource, subresource string, operation Operation, operationOptions runtime.Object, dryRun bool, userInfo user.Info) Attributes { func NewAttributesRecord(object runtime.Object, oldObject runtime.Object, kind schema.GroupVersionKind, namespace, name string, resource schema.GroupVersionResource, subresource string, operation Operation, operationOptions runtime.Object, dryRun bool, userInfo user.Info) Attributes {
return &attributesRecord{ return &attributesRecord{
kind: kind, kind: kind,
@ -111,7 +117,7 @@ func (record *attributesRecord) GetUserInfo() user.Info {
// getAnnotations implements privateAnnotationsGetter.It's a private method used // getAnnotations implements privateAnnotationsGetter.It's a private method used
// by WithAudit decorator. // by WithAudit decorator.
func (record *attributesRecord) getAnnotations() map[string]string { func (record *attributesRecord) getAnnotations(maxLevel auditinternal.Level) map[string]string {
record.annotationsLock.RLock() record.annotationsLock.RLock()
defer record.annotationsLock.RUnlock() defer record.annotationsLock.RUnlock()
@ -120,26 +126,36 @@ func (record *attributesRecord) getAnnotations() map[string]string {
} }
cp := make(map[string]string, len(record.annotations)) cp := make(map[string]string, len(record.annotations))
for key, value := range record.annotations { for key, value := range record.annotations {
cp[key] = value if value.level.Less(maxLevel) || value.level == maxLevel {
cp[key] = value.value
}
} }
return cp return cp
} }
// AddAnnotation adds an annotation to attributesRecord with Metadata audit level
func (record *attributesRecord) AddAnnotation(key, value string) error { func (record *attributesRecord) AddAnnotation(key, value string) error {
return record.AddAnnotationWithLevel(key, value, auditinternal.LevelMetadata)
}
func (record *attributesRecord) AddAnnotationWithLevel(key, value string, level auditinternal.Level) error {
if err := checkKeyFormat(key); err != nil { if err := checkKeyFormat(key); err != nil {
return err return err
} }
if level.Less(auditinternal.LevelMetadata) {
return fmt.Errorf("admission annotations are not allowed to be set at audit level lower than Metadata, key: %q, level: %s", key, level)
}
record.annotationsLock.Lock() record.annotationsLock.Lock()
defer record.annotationsLock.Unlock() defer record.annotationsLock.Unlock()
if record.annotations == nil { if record.annotations == nil {
record.annotations = make(map[string]string) record.annotations = make(map[string]annotation)
} }
if v, ok := record.annotations[key]; ok && v != value { annotation := annotation{level: level, value: value}
return fmt.Errorf("admission annotations are not allowd to be overwritten, key:%q, old value: %q, new value:%q", key, record.annotations[key], value) if v, ok := record.annotations[key]; ok && v != annotation {
return fmt.Errorf("admission annotations are not allowd to be overwritten, key:%q, old value: %v, new value: %v", key, record.annotations[key], annotation)
} }
record.annotations[key] = value record.annotations[key] = annotation
return nil return nil
} }

View File

@ -20,6 +20,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
) )
func TestAddAnnotation(t *testing.T) { func TestAddAnnotation(t *testing.T) {
@ -28,13 +29,13 @@ func TestAddAnnotation(t *testing.T) {
// test AddAnnotation // test AddAnnotation
attr.AddAnnotation("podsecuritypolicy.admission.k8s.io/validate-policy", "privileged") attr.AddAnnotation("podsecuritypolicy.admission.k8s.io/validate-policy", "privileged")
attr.AddAnnotation("podsecuritypolicy.admission.k8s.io/admit-policy", "privileged") attr.AddAnnotation("podsecuritypolicy.admission.k8s.io/admit-policy", "privileged")
annotations := attr.getAnnotations() annotations := attr.getAnnotations(auditinternal.LevelMetadata)
assert.Equal(t, annotations["podsecuritypolicy.admission.k8s.io/validate-policy"], "privileged") assert.Equal(t, annotations["podsecuritypolicy.admission.k8s.io/validate-policy"], "privileged")
// test overwrite // test overwrite
assert.Error(t, attr.AddAnnotation("podsecuritypolicy.admission.k8s.io/validate-policy", "privileged-overwrite"), assert.Error(t, attr.AddAnnotation("podsecuritypolicy.admission.k8s.io/validate-policy", "privileged-overwrite"),
"admission annotations should not be allowd to be overwritten") "admission annotations should not be allowd to be overwritten")
annotations = attr.getAnnotations() annotations = attr.getAnnotations(auditinternal.LevelMetadata)
assert.Equal(t, annotations["podsecuritypolicy.admission.k8s.io/validate-policy"], "privileged", "admission annotations should not be overwritten") assert.Equal(t, annotations["podsecuritypolicy.admission.k8s.io/validate-policy"], "privileged", "admission annotations should not be overwritten")
// test invalid plugin names // test invalid plugin names
@ -47,7 +48,7 @@ func TestAddAnnotation(t *testing.T) {
for name, invalidKey := range testCases { for name, invalidKey := range testCases {
err := attr.AddAnnotation(invalidKey, "value-foo") err := attr.AddAnnotation(invalidKey, "value-foo")
assert.Error(t, err) assert.Error(t, err)
annotations = attr.getAnnotations() annotations = attr.getAnnotations(auditinternal.LevelMetadata)
assert.Equal(t, annotations[invalidKey], "", name+": invalid pluginName is not allowed ") assert.Equal(t, annotations[invalidKey], "", name+": invalid pluginName is not allowed ")
} }

View File

@ -84,11 +84,18 @@ func ensureAnnotationGetter(a Attributes) error {
} }
func (handler auditHandler) logAnnotations(a Attributes) { func (handler auditHandler) logAnnotations(a Attributes) {
if handler.ae == nil {
return
}
switch a := a.(type) { switch a := a.(type) {
case privateAnnotationsGetter: case privateAnnotationsGetter:
audit.LogAnnotations(handler.ae, a.getAnnotations()) for key, value := range a.getAnnotations(handler.ae.Level) {
audit.LogAnnotation(handler.ae, key, value)
}
case AnnotationsGetter: case AnnotationsGetter:
audit.LogAnnotations(handler.ae, a.GetAnnotations()) for key, value := range a.GetAnnotations(handler.ae.Level) {
audit.LogAnnotation(handler.ae, key, value)
}
default: default:
// this will never happen, because we have already checked it in ensureAnnotationGetter // this will never happen, because we have already checked it in ensureAnnotationGetter
} }

View File

@ -21,6 +21,7 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
) )
@ -61,8 +62,15 @@ type Attributes interface {
// "podsecuritypolicy" is the name of the plugin, "admission.k8s.io" is the name of the organization, "admit-policy" is the key name. // "podsecuritypolicy" is the name of the plugin, "admission.k8s.io" is the name of the organization, "admit-policy" is the key name.
// An error is returned if the format of key is invalid. When trying to overwrite annotation with a new value, an error is returned. // An error is returned if the format of key is invalid. When trying to overwrite annotation with a new value, an error is returned.
// Both ValidationInterface and MutationInterface are allowed to add Annotations. // Both ValidationInterface and MutationInterface are allowed to add Annotations.
// By default, an annotation gets logged into audit event if the request's audit level is greater or
// equal to Metadata.
AddAnnotation(key, value string) error AddAnnotation(key, value string) error
// AddAnnotationWithLevel sets annotation according to key-value pair with additional intended audit level.
// An Annotation gets logged into audit event if the request's audit level is greater or equal to the
// intended audit level.
AddAnnotationWithLevel(key, value string, level auditinternal.Level) error
// GetReinvocationContext tracks the admission request information relevant to the re-invocation policy. // GetReinvocationContext tracks the admission request information relevant to the re-invocation policy.
GetReinvocationContext() ReinvocationContext GetReinvocationContext() ReinvocationContext
} }
@ -85,13 +93,13 @@ type ObjectInterfaces interface {
// privateAnnotationsGetter is a private interface which allows users to get annotations from Attributes. // privateAnnotationsGetter is a private interface which allows users to get annotations from Attributes.
type privateAnnotationsGetter interface { type privateAnnotationsGetter interface {
getAnnotations() map[string]string getAnnotations(maxLevel auditinternal.Level) map[string]string
} }
// AnnotationsGetter allows users to get annotations from Attributes. An alternate Attribute should implement // AnnotationsGetter allows users to get annotations from Attributes. An alternate Attribute should implement
// this interface. // this interface.
type AnnotationsGetter interface { type AnnotationsGetter interface {
GetAnnotations() map[string]string GetAnnotations(maxLevel auditinternal.Level) map[string]string
} }
// ReinvocationContext provides access to the admission related state required to implement the re-invocation policy. // ReinvocationContext provides access to the admission related state required to implement the re-invocation policy.

View File

@ -29,6 +29,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
webhooktesting "k8s.io/apiserver/pkg/admission/plugin/webhook/testing" webhooktesting "k8s.io/apiserver/pkg/admission/plugin/webhook/testing"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
) )
// TestAdmit tests that MutatingWebhook#Admit works as expected // TestAdmit tests that MutatingWebhook#Admit works as expected
@ -108,9 +109,9 @@ func TestAdmit(t *testing.T) {
return return
} }
if len(tt.ExpectAnnotations) == 0 { if len(tt.ExpectAnnotations) == 0 {
assert.Empty(t, fakeAttr.GetAnnotations(), tt.Name+": annotations not set as expected.") assert.Empty(t, fakeAttr.GetAnnotations(auditinternal.LevelMetadata), tt.Name+": annotations not set as expected.")
} else { } else {
assert.Equal(t, tt.ExpectAnnotations, fakeAttr.GetAnnotations(), tt.Name+": annotations not set as expected.") assert.Equal(t, tt.ExpectAnnotations, fakeAttr.GetAnnotations(auditinternal.LevelMetadata), tt.Name+": annotations not set as expected.")
} }
reinvocationCtx := fakeAttr.Attributes.GetReinvocationContext() reinvocationCtx := fakeAttr.Attributes.GetReinvocationContext()
reinvocationCtx.SetIsReinvoke() reinvocationCtx.SetIsReinvoke()

View File

@ -25,6 +25,7 @@ import (
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
webhooktesting "k8s.io/apiserver/pkg/admission/plugin/webhook/testing" webhooktesting "k8s.io/apiserver/pkg/admission/plugin/webhook/testing"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
) )
// TestValidate tests that ValidatingWebhook#Validate works as expected // TestValidate tests that ValidatingWebhook#Validate works as expected
@ -86,9 +87,9 @@ func TestValidate(t *testing.T) {
continue continue
} }
if len(tt.ExpectAnnotations) == 0 { if len(tt.ExpectAnnotations) == 0 {
assert.Empty(t, fakeAttr.GetAnnotations(), tt.Name+": annotations not set as expected.") assert.Empty(t, fakeAttr.GetAnnotations(auditinternal.LevelMetadata), tt.Name+": annotations not set as expected.")
} else { } else {
assert.Equal(t, tt.ExpectAnnotations, fakeAttr.GetAnnotations(), tt.Name+": annotations not set as expected.") assert.Equal(t, tt.ExpectAnnotations, fakeAttr.GetAnnotations(auditinternal.LevelMetadata), tt.Name+": annotations not set as expected.")
} }
} }
} }

View File

@ -230,16 +230,6 @@ func LogAnnotation(ae *auditinternal.Event, key, value string) {
ae.Annotations[key] = value ae.Annotations[key] = value
} }
// LogAnnotations fills in the Annotations according to the annotations map.
func LogAnnotations(ae *auditinternal.Event, annotations map[string]string) {
if ae == nil || ae.Level.Less(auditinternal.LevelMetadata) {
return
}
for key, value := range annotations {
LogAnnotation(ae, key, value)
}
}
// truncate User-Agent if too long, otherwise return it directly. // truncate User-Agent if too long, otherwise return it directly.
func maybeTruncateUserAgent(req *http.Request) string { func maybeTruncateUserAgent(req *http.Request) string {
ua := req.UserAgent() ua := req.UserAgent()