Merge pull request #46065 from timstclair/audit-api

Automatic merge from submit-queue (batch tested with PRs 45913, 46065, 46352, 46363, 46373)

Update audit API with missing pieces

Follow-up to https://github.com/kubernetes/kubernetes/pull/45315 to resolve pending decisions & issues, including:

- Audit ID format
- Identifying audit event "stage"
- Request/Response object format (resolve conversion issue)
- Add a subresource field to the `ObjectReference`

For https://github.com/kubernetes/features/issues/22

~~TODO: Add generated code once we've reached consensus on the types.~~

/cc @deads2k @ihmccreery @sttts @soltysh @ericchiang
This commit is contained in:
Kubernetes Submit Queue 2017-05-25 00:11:01 -07:00 committed by GitHub
commit 74f501935b
8 changed files with 699 additions and 502 deletions

View File

@ -22,6 +22,14 @@ import (
"k8s.io/apimachinery/pkg/types"
)
// Header keys used by the audit system.
const (
// Header to hold the audit ID as the request is propagated through the serving hierarchy. The
// Audit-ID header should be set by the first server to receive the request (e.g. the federation
// server or kube-aggregator).
HeaderAuditID = "Audit-ID"
)
// Level defines the amount of information logged during auditing
type Level string
@ -39,6 +47,22 @@ const (
LevelRequestResponse Level = "RequestResponse"
)
// Stage defines the stages in request handling that audit events may be generated.
type Stage string
// Valid audit stages.
const (
// The stage for events generated as soon as the audit handler receives the request, and before it
// is delegated down the handler chain.
StageRequestReceived = "RequestReceived"
// The stage for events generated once the response headers are sent, but before the response body
// is sent. This stage is only generated for long-running requests (e.g. watch).
StageResponseStarted = "ResponseStarted"
// The stage for events generated once the response body has been completed, and no more bytes
// will be sent.
StageResponseComplete = "ResponseComplete"
)
// Event captures all the information that can be included in an API audit log.
type Event struct {
metav1.TypeMeta
@ -53,6 +77,9 @@ type Event struct {
Timestamp metav1.Time
// Unique audit ID, generated for each request.
AuditID types.UID
// Stage of the request handling when this event instance was generated.
Stage Stage
// RequestURI is the request URI as sent by the client to a server.
RequestURI string
// Verb is the kubernetes verb associated with the request.
@ -81,12 +108,12 @@ type Event struct {
// merging. It is an external versioned object type, and may not be a valid object on its own.
// Omitted for non-resource requests. Only logged at Request Level and higher.
// +optional
RequestObject runtime.Unknown
RequestObject *runtime.Unknown
// API object returned in the response, in JSON. The ResponseObject is recorded after conversion
// to the external type, and serialized as JSON. Omitted for non-resource requests. Only logged
// at Response Level.
// +optional
ResponseObject runtime.Unknown
ResponseObject *runtime.Unknown
}
// EventList is a list of audit Events.
@ -191,6 +218,8 @@ type ObjectReference struct {
APIVersion string
// +optional
ResourceVersion string
// +optional
Subresource string
}
// UserInfo holds the information about the user needed to implement the

View File

@ -23,6 +23,14 @@ import (
authnv1 "k8s.io/client-go/pkg/apis/authentication/v1"
)
// Header keys used by the audit system.
const (
// Header to hold the audit ID as the request is propagated through the serving hierarchy. The
// Audit-ID header should be set by the first server to receive the request (e.g. the federation
// server or kube-aggregator).
HeaderAuditID = "Audit-ID"
)
// Level defines the amount of information logged during auditing
type Level string
@ -40,6 +48,22 @@ const (
LevelRequestResponse Level = "RequestResponse"
)
// Stage defines the stages in request handling that audit events may be generated.
type Stage string
// Valid audit stages.
const (
// The stage for events generated as soon as the audit handler receives the request, and before it
// is delegated down the handler chain.
StageRequestReceived = "RequestReceived"
// The stage for events generated once the response headers are sent, but before the response body
// is sent. This stage is only generated for long-running requests (e.g. watch).
StageResponseStarted = "ResponseStarted"
// The stage for events generated once the response body has been completed, and no more bytes
// will be sent.
StageResponseComplete = "ResponseComplete"
)
// Event captures all the information that can be included in an API audit log.
type Event struct {
metav1.TypeMeta `json:",inline"`
@ -53,7 +77,10 @@ type Event struct {
// Time the request reached the apiserver.
Timestamp metav1.Time `json:"timestamp"`
// Unique audit ID, generated for each request.
AuditID types.UID `json:"auditID,omitempty"`
AuditID types.UID `json:"auditID"`
// Stage of the request handling when this event instance was generated.
Stage Stage `json:"stage"`
// RequestURI is the request URI as sent by the client to a server.
RequestURI string `json:"requestURI"`
// Verb is the kubernetes verb associated with the request.
@ -82,12 +109,12 @@ type Event struct {
// merging. It is an external versioned object type, and may not be a valid object on its own.
// Omitted for non-resource requests. Only logged at Request Level and higher.
// +optional
RequestObject runtime.RawExtension `json:"requestObject,omitempty"`
RequestObject *runtime.Unknown `json:"requestObject,omitempty"`
// API object returned in the response, in JSON. The ResponseObject is recorded after conversion
// to the external type, and serialized as JSON. Omitted for non-resource requests. Only logged
// at Response Level.
// +optional
ResponseObject runtime.RawExtension `json:"responseObject,omitempty"`
ResponseObject *runtime.Unknown `json:"responseObject,omitempty"`
}
// EventList is a list of audit Events.
@ -192,4 +219,6 @@ type ObjectReference struct {
APIVersion string `json:"apiVersion,omitempty"`
// +optional
ResourceVersion string `json:"resourceVersion,omitempty"`
// +optional
Subresource string `json:"subresource,omitempty"`
}

View File

@ -60,6 +60,7 @@ func autoConvert_v1alpha1_Event_To_audit_Event(in *Event, out *audit.Event, s co
out.Level = audit.Level(in.Level)
out.Timestamp = in.Timestamp
out.AuditID = types.UID(in.AuditID)
out.Stage = audit.Stage(in.Stage)
out.RequestURI = in.RequestURI
out.Verb = in.Verb
// TODO: Inefficient conversion - can we improve it?
@ -70,14 +71,8 @@ func autoConvert_v1alpha1_Event_To_audit_Event(in *Event, out *audit.Event, s co
out.SourceIPs = *(*[]string)(unsafe.Pointer(&in.SourceIPs))
out.ObjectRef = (*audit.ObjectReference)(unsafe.Pointer(in.ObjectRef))
out.ResponseStatus = (*v1.Status)(unsafe.Pointer(in.ResponseStatus))
// TODO: Inefficient conversion - can we improve it?
if err := s.Convert(&in.RequestObject, &out.RequestObject, 0); err != nil {
return err
}
// TODO: Inefficient conversion - can we improve it?
if err := s.Convert(&in.ResponseObject, &out.ResponseObject, 0); err != nil {
return err
}
out.RequestObject = (*runtime.Unknown)(unsafe.Pointer(in.RequestObject))
out.ResponseObject = (*runtime.Unknown)(unsafe.Pointer(in.ResponseObject))
return nil
}
@ -91,6 +86,7 @@ func autoConvert_audit_Event_To_v1alpha1_Event(in *audit.Event, out *Event, s co
out.Level = Level(in.Level)
out.Timestamp = in.Timestamp
out.AuditID = types.UID(in.AuditID)
out.Stage = Stage(in.Stage)
out.RequestURI = in.RequestURI
out.Verb = in.Verb
// TODO: Inefficient conversion - can we improve it?
@ -101,14 +97,8 @@ func autoConvert_audit_Event_To_v1alpha1_Event(in *audit.Event, out *Event, s co
out.SourceIPs = *(*[]string)(unsafe.Pointer(&in.SourceIPs))
out.ObjectRef = (*ObjectReference)(unsafe.Pointer(in.ObjectRef))
out.ResponseStatus = (*v1.Status)(unsafe.Pointer(in.ResponseStatus))
// TODO: Inefficient conversion - can we improve it?
if err := s.Convert(&in.RequestObject, &out.RequestObject, 0); err != nil {
return err
}
// TODO: Inefficient conversion - can we improve it?
if err := s.Convert(&in.ResponseObject, &out.ResponseObject, 0); err != nil {
return err
}
out.RequestObject = (*runtime.Unknown)(unsafe.Pointer(in.RequestObject))
out.ResponseObject = (*runtime.Unknown)(unsafe.Pointer(in.ResponseObject))
return nil
}
@ -119,17 +109,7 @@ func Convert_audit_Event_To_v1alpha1_Event(in *audit.Event, out *Event, s conver
func autoConvert_v1alpha1_EventList_To_audit_EventList(in *EventList, out *audit.EventList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]audit.Event, len(*in))
for i := range *in {
if err := Convert_v1alpha1_Event_To_audit_Event(&(*in)[i], &(*out)[i], s); err != nil {
return err
}
}
} else {
out.Items = nil
}
out.Items = *(*[]audit.Event)(unsafe.Pointer(&in.Items))
return nil
}
@ -140,16 +120,10 @@ func Convert_v1alpha1_EventList_To_audit_EventList(in *EventList, out *audit.Eve
func autoConvert_audit_EventList_To_v1alpha1_EventList(in *audit.EventList, out *EventList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Event, len(*in))
for i := range *in {
if err := Convert_audit_Event_To_v1alpha1_Event(&(*in)[i], &(*out)[i], s); err != nil {
return err
}
}
} else {
if in.Items == nil {
out.Items = make([]Event, 0)
} else {
out.Items = *(*[]Event)(unsafe.Pointer(&in.Items))
}
return nil
}
@ -188,6 +162,7 @@ func autoConvert_v1alpha1_ObjectReference_To_audit_ObjectReference(in *ObjectRef
out.UID = types.UID(in.UID)
out.APIVersion = in.APIVersion
out.ResourceVersion = in.ResourceVersion
out.Subresource = in.Subresource
return nil
}
@ -203,6 +178,7 @@ func autoConvert_audit_ObjectReference_To_v1alpha1_ObjectReference(in *audit.Obj
out.UID = types.UID(in.UID)
out.APIVersion = in.APIVersion
out.ResourceVersion = in.ResourceVersion
out.Subresource = in.Subresource
return nil
}

View File

@ -89,15 +89,21 @@ func DeepCopy_v1alpha1_Event(in interface{}, out interface{}, c *conversion.Clon
*out = newVal.(*v1.Status)
}
}
if newVal, err := c.DeepCopy(&in.RequestObject); err != nil {
return err
} else {
out.RequestObject = *newVal.(*runtime.RawExtension)
if in.RequestObject != nil {
in, out := &in.RequestObject, &out.RequestObject
if newVal, err := c.DeepCopy(*in); err != nil {
return err
} else {
*out = newVal.(*runtime.Unknown)
}
}
if newVal, err := c.DeepCopy(&in.ResponseObject); err != nil {
return err
} else {
out.ResponseObject = *newVal.(*runtime.RawExtension)
if in.ResponseObject != nil {
in, out := &in.ResponseObject, &out.ResponseObject
if newVal, err := c.DeepCopy(*in); err != nil {
return err
} else {
*out = newVal.(*runtime.Unknown)
}
}
return nil
}

View File

@ -89,15 +89,21 @@ func DeepCopy_audit_Event(in interface{}, out interface{}, c *conversion.Cloner)
*out = newVal.(*v1.Status)
}
}
if newVal, err := c.DeepCopy(&in.RequestObject); err != nil {
return err
} else {
out.RequestObject = *newVal.(*runtime.Unknown)
if in.RequestObject != nil {
in, out := &in.RequestObject, &out.RequestObject
if newVal, err := c.DeepCopy(*in); err != nil {
return err
} else {
*out = newVal.(*runtime.Unknown)
}
}
if newVal, err := c.DeepCopy(&in.ResponseObject); err != nil {
return err
} else {
out.ResponseObject = *newVal.(*runtime.Unknown)
if in.ResponseObject != nil {
in, out := &in.ResponseObject, &out.ResponseObject
if newVal, err := c.DeepCopy(*in); err != nil {
return err
} else {
*out = newVal.(*runtime.Unknown)
}
}
return nil
}

View File

@ -40,10 +40,7 @@ import (
authenticationv1 "k8s.io/client-go/pkg/apis/authentication/v1"
)
const (
AuditIDHeader = "X-Request-ID"
)
// NewEventFromRequest generates an audit event for the request.
func NewEventFromRequest(req *http.Request, policy *auditinternal.Policy, attribs authorizer.Attributes) (*auditinternal.Event, error) {
ev := &auditinternal.Event{
Timestamp: metav1.NewTime(time.Now()),
@ -61,7 +58,7 @@ func NewEventFromRequest(req *http.Request, policy *auditinternal.Policy, attrib
// prefer the id from the headers. If not available, create a new one.
// TODO(audit): do we want to forbid the header for non-front-proxy users?
ids := req.Header[AuditIDHeader]
ids := req.Header[auditinternal.HeaderAuditID]
if len(ids) > 0 {
ev.AuditID = types.UID(ids[0])
} else {
@ -157,7 +154,7 @@ func LogRequestPatch(ae *audit.Event, patch []byte) {
return
}
ae.RequestObject = runtime.Unknown{
ae.RequestObject = &runtime.Unknown{
Raw: patch,
ContentType: runtime.ContentTypeJSON,
}
@ -182,21 +179,21 @@ func LogResponseObject(ae *audit.Event, obj runtime.Object, gv schema.GroupVersi
}
}
func encodeObject(obj runtime.Object, gv schema.GroupVersion, serializer runtime.NegotiatedSerializer) (runtime.Unknown, error) {
func encodeObject(obj runtime.Object, gv schema.GroupVersion, serializer runtime.NegotiatedSerializer) (*runtime.Unknown, error) {
supported := serializer.SupportedMediaTypes()
for i := range supported {
if supported[i].MediaType == "application/json" {
enc := serializer.EncoderForVersion(supported[i].Serializer, gv)
var buf bytes.Buffer
if err := enc.Encode(obj, &buf); err != nil {
return runtime.Unknown{}, fmt.Errorf("encoding failed: %v", err)
return nil, fmt.Errorf("encoding failed: %v", err)
}
return runtime.Unknown{
return &runtime.Unknown{
Raw: buf.Bytes(),
ContentType: runtime.ContentTypeJSON,
}, nil
}
}
return runtime.Unknown{}, fmt.Errorf("no json encoder found")
return nil, fmt.Errorf("no json encoder found")
}

View File

@ -65,8 +65,22 @@ func TestAudit(t *testing.T) {
simpleCPrimeJSON, _ := runtime.Encode(testCodec, simpleCPrime)
// event checks
noRequestBody := func(i int) eventCheck {
return func(events []*auditinternal.Event) error {
if events[i].RequestObject == nil {
return nil
}
return fmt.Errorf("expected RequestBody to be nil, got non-nill '%s'", events[i].RequestObject.Raw)
}
}
requestBodyIs := func(i int, text string) eventCheck {
return func(events []*auditinternal.Event) error {
if events[i].RequestObject == nil {
if text != "" {
return fmt.Errorf("expected RequestBody %q, got <nil>", text)
}
return nil
}
if string(events[i].RequestObject.Raw) != text {
return fmt.Errorf("expected RequestBody %q, got %q", text, string(events[i].RequestObject.Raw))
}
@ -81,12 +95,12 @@ func TestAudit(t *testing.T) {
return nil
}
}
responseBodyIs := func(i int, text string) eventCheck {
noResponseBody := func(i int) eventCheck {
return func(events []*auditinternal.Event) error {
if string(events[i].ResponseObject.Raw) != text {
return fmt.Errorf("expected ResponseBody %q, got %q", text, string(events[i].ResponseObject.Raw))
if events[i].ResponseObject == nil {
return nil
}
return nil
return fmt.Errorf("expected ResponseBody to be nil, got non-nill '%s'", events[i].ResponseObject.Raw)
}
}
responseBodyMatches := func(i int, pattern string) eventCheck {
@ -115,7 +129,7 @@ func TestAudit(t *testing.T) {
200,
1,
[]eventCheck{
requestBodyIs(0, ""),
noRequestBody(0),
responseBodyMatches(0, `{.*"name":"c".*}`),
},
},
@ -132,7 +146,7 @@ func TestAudit(t *testing.T) {
200,
1,
[]eventCheck{
requestBodyMatches(0, ""),
noRequestBody(0),
responseBodyMatches(0, `{.*"name":"a".*"name":"b".*}`),
},
},
@ -158,8 +172,8 @@ func TestAudit(t *testing.T) {
405,
1,
[]eventCheck{
requestBodyIs(0, ""), // the 405 is thrown long before the create handler would be executed
responseBodyIs(0, ""), // the 405 is thrown long before the create handler would be executed
noRequestBody(0), // the 405 is thrown long before the create handler would be executed
noResponseBody(0), // the 405 is thrown long before the create handler would be executed
},
},
{
@ -171,8 +185,8 @@ func TestAudit(t *testing.T) {
200,
1,
[]eventCheck{
requestBodyMatches(0, ""),
responseBodyMatches(0, ""),
noRequestBody(0),
responseBodyMatches(0, `{.*"kind":"Status".*"status":"Success".*}`),
},
},
{
@ -185,7 +199,7 @@ func TestAudit(t *testing.T) {
1,
[]eventCheck{
requestBodyMatches(0, "DeleteOptions"),
responseBodyMatches(0, ""),
responseBodyMatches(0, `{.*"kind":"Status".*"status":"Success".*}`),
},
},
{
@ -247,8 +261,8 @@ func TestAudit(t *testing.T) {
200,
2,
[]eventCheck{
requestBodyMatches(0, ""),
responseBodyMatches(0, ""),
noRequestBody(0),
noResponseBody(0),
},
},
} {