mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-25 04:33:26 +00:00
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:
commit
74f501935b
@ -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
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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"`
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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),
|
||||
},
|
||||
},
|
||||
} {
|
||||
|
Loading…
Reference in New Issue
Block a user