diff --git a/audit/api/api_common.go b/audit/api/api_common.go index f7754fc..43458d3 100644 --- a/audit/api/api_common.go +++ b/audit/api/api_common.go @@ -104,7 +104,7 @@ func validateAndSerializePartially( return nil, err } - // Test serialization even if the data is dropped later while logging to the legacy solution + // Test serialization even if the data is dropped later when logging to the legacy solution auditEventBytes, err := proto.Marshal(event) if err != nil { return nil, err diff --git a/audit/api/builder.go b/audit/api/builder.go index 2c41b73..68614d2 100644 --- a/audit/api/builder.go +++ b/audit/api/builder.go @@ -90,7 +90,7 @@ func NewAuditLogEntryBuilder() *AuditLogEntryBuilder { EventType: EventTypeAdminActivity, }, auditRequest: AuditRequest{ - Request: nil, + Request: &ApiRequest{}, RequestClientIP: "0.0.0.0", RequestCorrelationId: nil, RequestId: nil, @@ -120,6 +120,35 @@ func NewAuditLogEntryBuilder() *AuditLogEntryBuilder { } } +func (builder *AuditLogEntryBuilder) AsSystemEvent() *AuditLogEntryBuilder { + if builder.auditRequest.Request == nil { + builder.auditRequest.Request = &ApiRequest{} + } + if builder.auditRequest.Request.Header == nil { + builder.auditRequest.Request.Header = map[string][]string{"user-agent": {"none"}} + } + if builder.auditRequest.Request.Host == "" { + builder.auditRequest.Request.Host = "0.0.0.0" + } + if builder.auditRequest.Request.Method == "" { + builder.auditRequest.Request.Method = "OTHER" + } + if builder.auditRequest.Request.Scheme == "" { + builder.auditRequest.Request.Scheme = "none" + } + if builder.auditRequest.Request.Proto == "" { + builder.auditRequest.Request.Proto = "none" + } + if builder.auditRequest.Request.URL.Path == "" { + builder.auditRequest.Request.URL.Path = "none" + } + if builder.auditRequest.RequestClientIP == "" { + builder.auditRequest.RequestClientIP = "0.0.0.0" + } + builder.WithEventType(EventTypeSystemEvent) + return builder +} + // WithRequiredApiRequest adds api request details func (builder *AuditLogEntryBuilder) WithRequiredApiRequest(request ApiRequest) *AuditLogEntryBuilder { builder.auditRequest.Request = &request @@ -398,6 +427,12 @@ func (builder *AuditEventBuilder) RevertSequenceNumber() { (*builder.sequenceNumberGenerator).Revert() } +func (builder *AuditEventBuilder) AsSystemEvent() *AuditEventBuilder { + builder.auditLogEntryBuilder.AsSystemEvent() + builder.WithVisibility(auditV1.Visibility_VISIBILITY_PRIVATE) + return builder +} + // WithAuditLogEntryBuilder overwrites the preconfigured AuditLogEntryBuilder func (builder *AuditEventBuilder) WithAuditLogEntryBuilder(auditLogEntryBuilder *AuditLogEntryBuilder) *AuditEventBuilder { builder.auditLogEntryBuilder = auditLogEntryBuilder diff --git a/audit/api/builder_test.go b/audit/api/builder_test.go index 8feab54..cdd68d4 100644 --- a/audit/api/builder_test.go +++ b/audit/api/builder_test.go @@ -71,6 +71,29 @@ func Test_getObjectIdAndTypeFromAuditParams(t *testing.T) { func Test_AuditLogEntryBuilder(t *testing.T) { + t.Run("nothing set", func(t *testing.T) { + logEntry, err := NewAuditLogEntryBuilder().Build(context.Background(), SequenceNumber(1)) + assert.Error(t, err) + assert.Equal(t, "object id missing", err.Error()) + assert.Nil(t, logEntry) + }) + + t.Run("details missing", func(t *testing.T) { + logEntry, err := NewAuditLogEntryBuilder().WithRequiredLocation("eu01"). + WithRequiredObjectId("1"). + WithRequiredObjectType(SingularTypeProject). + Build(context.Background(), SequenceNumber(1)) + + assert.NoError(t, err) + assert.NotNil(t, logEntry) + + validator, err := protovalidate.New() + assert.NoError(t, err) + err = validator.Validate(logEntry) + assert.Error(t, err) + assert.Equal(t, "validation error:\n - proto_payload.service_name: value is required [required]\n - proto_payload.operation_name: value is required [required]\n - proto_payload.request_metadata.caller_supplied_user_agent: value is required [required]\n - proto_payload.request_metadata.request_attributes.method: value is required [required]\n - proto_payload.request_metadata.request_attributes.headers: value is required [required]\n - proto_payload.request_metadata.request_attributes.path: value is required [required]\n - proto_payload.request_metadata.request_attributes.host: value is required [required]\n - proto_payload.request_metadata.request_attributes.scheme: value is required [required]\n - proto_payload.request_metadata.request_attributes.protocol: value is required [required]\n - insert_id: value does not match regex pattern `^[0-9]+/[a-z0-9-]+/[a-z0-9-]+/[0-9]+$` [string.pattern]", err.Error()) + }) + t.Run("required only", func(t *testing.T) { builder := NewAuditLogEntryBuilder(). WithRequiredLocation("eu01"). @@ -199,7 +222,7 @@ func Test_AuditLogEntryBuilder(t *testing.T) { WithAuditPermission(permission). WithAuditPermissionCheckResult(permissionCheckResult). WithDetails(details). - WithEventType(EventTypeSystemEvent). + WithEventType(EventTypePolicyDenied). WithLabels(map[string]string{"key": "label"}). WithNumResponseItems(int64(10)). WithRequestCorrelationId("correlationId"). @@ -215,7 +238,7 @@ func Test_AuditLogEntryBuilder(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, logEntry) - assert.Equal(t, "projects/1/logs/system-event", logEntry.LogName) + assert.Equal(t, "projects/1/logs/policy-denied", logEntry.LogName) assert.Equal(t, map[string]string{"key": "label"}, logEntry.Labels) assert.Nil(t, logEntry.TraceState) assert.Nil(t, logEntry.TraceParent) @@ -292,6 +315,91 @@ func Test_AuditLogEntryBuilder(t *testing.T) { assert.NoError(t, err) }) + t.Run("system event", func(t *testing.T) { + builder := NewAuditLogEntryBuilder(). + WithRequiredLocation("eu01"). + WithRequiredObjectId("1"). + WithRequiredObjectType(SingularTypeProject). + WithRequiredOperation("stackit.demo-service.v1.operation"). + WithRequiredServiceName("demo-service"). + WithRequiredWorkerId("worker-id"). + AsSystemEvent() + + logEntry, err := builder.Build(context.Background(), SequenceNumber(1)) + assert.NoError(t, err) + assert.NotNil(t, logEntry) + + assert.Equal(t, "projects/1/logs/system-event", logEntry.LogName) + assert.Nil(t, logEntry.Labels) + assert.Nil(t, logEntry.TraceState) + assert.Nil(t, logEntry.TraceParent) + assert.Equal(t, auditV1.LogSeverity_LOG_SEVERITY_DEFAULT, logEntry.Severity) + assert.NotNil(t, logEntry.Timestamp) + assert.Nil(t, logEntry.CorrelationId) + assert.Regexp(t, "[0-9]+/eu01/worker-id/1", logEntry.InsertId) + + assert.NotNil(t, logEntry.ProtoPayload) + + authenticationInfo := logEntry.ProtoPayload.AuthenticationInfo + assert.NotNil(t, authenticationInfo) + assert.Equal(t, "do-not-reply@stackit.cloud", authenticationInfo.PrincipalEmail) + assert.Equal(t, "none", authenticationInfo.PrincipalId) + assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo) + assert.Nil(t, authenticationInfo.ServiceAccountName) + + assert.Nil(t, logEntry.ProtoPayload.AuthorizationInfo) + assert.Nil(t, logEntry.ProtoPayload.Metadata) + assert.Equal(t, "stackit.demo-service.v1.operation", logEntry.ProtoPayload.OperationName) + assert.Nil(t, logEntry.ProtoPayload.Request) + + requestMetadata := logEntry.ProtoPayload.RequestMetadata + assert.NotNil(t, requestMetadata) + assert.Equal(t, "0.0.0.0", requestMetadata.CallerIp) + assert.Equal(t, "none", requestMetadata.CallerSuppliedUserAgent) + + requestAttributes := requestMetadata.RequestAttributes + assert.NotNil(t, requestAttributes) + assert.Equal(t, "none", requestAttributes.Path) + assert.NotNil(t, requestAttributes.Time) + assert.Equal(t, "0.0.0.0", requestAttributes.Host) + assert.Equal(t, auditV1.AttributeContext_HTTP_METHOD_OTHER, requestAttributes.Method) + assert.Nil(t, requestAttributes.Id) + assert.Equal(t, "none", requestAttributes.Scheme) + assert.Equal(t, map[string]string{"user-agent": "none"}, requestAttributes.Headers) + assert.Nil(t, requestAttributes.Query) + assert.Equal(t, "none", requestAttributes.Protocol) + + requestAttributesAuth := requestAttributes.Auth + assert.NotNil(t, requestAttributesAuth) + assert.Equal(t, "none/none", requestAttributesAuth.Principal) + assert.Equal(t, []string{}, requestAttributesAuth.Audiences) + assert.NotNil(t, requestAttributesAuth.Claims) + assert.Equal(t, map[string]any{}, requestAttributesAuth.Claims.AsMap()) + + assert.Equal(t, "projects/1", logEntry.ProtoPayload.ResourceName) + assert.Nil(t, logEntry.ProtoPayload.Response) + + responseMetadata := logEntry.ProtoPayload.ResponseMetadata + assert.NotNil(t, responseMetadata) + assert.Nil(t, responseMetadata.ErrorDetails) + assert.Nil(t, responseMetadata.ErrorMessage) + assert.Equal(t, wrapperspb.Int32(200), responseMetadata.StatusCode) + + responseAttributes := responseMetadata.ResponseAttributes + assert.NotNil(t, responseAttributes) + assert.Nil(t, responseAttributes.Headers) + assert.Nil(t, responseAttributes.NumResponseItems) + assert.Nil(t, responseAttributes.Size) + assert.NotNil(t, responseAttributes.Time) + + assert.Equal(t, "demo-service", logEntry.ProtoPayload.ServiceName) + + validator, err := protovalidate.New() + assert.NoError(t, err) + err = validator.Validate(logEntry) + assert.NoError(t, err) + }) + t.Run("with response body unserialized", func(t *testing.T) { details := map[string]interface{}{"key": "detail"} permission := "project.edit" @@ -412,6 +520,38 @@ func Test_AuditLogEntryBuilder(t *testing.T) { func Test_AuditEventBuilder(t *testing.T) { + t.Run("nothing set", func(t *testing.T) { + api, _ := NewMockAuditApi() + sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator() + tracer := otel.Tracer("test") + + cloudEvent, routingIdentifier, op, err := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", "worker-id", "eu01"). + Build(context.Background(), SequenceNumber(1)) + + assert.Error(t, err) + assert.Equal(t, "object id missing", err.Error()) + assert.Nil(t, cloudEvent) + assert.Nil(t, routingIdentifier) + assert.Equal(t, "", op) + }) + + t.Run("details missing", func(t *testing.T) { + api, _ := NewMockAuditApi() + sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator() + tracer := otel.Tracer("test") + + cloudEvent, routingIdentifier, op, err := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", "worker-id", "eu01"). + WithRequiredObjectId("objectId"). + WithRequiredObjectType(SingularTypeProject). + Build(context.Background(), SequenceNumber(1)) + + assert.Error(t, err) + assert.Equal(t, "validation error:\n - log_name: value does not match regex pattern `^[a-z-]+/[a-z0-9-]+/logs/(?:admin-activity|system-event|policy-denied|data-access)$` [string.pattern]\n - proto_payload.operation_name: value is required [required]\n - proto_payload.resource_name: value does not match regex pattern `^[a-z]+/[a-z0-9-]+(?:/[a-z0-9-]+/[a-z0-9-_]+)*$` [string.pattern]\n - proto_payload.request_metadata.caller_supplied_user_agent: value is required [required]\n - proto_payload.request_metadata.request_attributes.method: value is required [required]\n - proto_payload.request_metadata.request_attributes.headers: value is required [required]\n - proto_payload.request_metadata.request_attributes.path: value is required [required]\n - proto_payload.request_metadata.request_attributes.host: value is required [required]\n - proto_payload.request_metadata.request_attributes.scheme: value is required [required]\n - proto_payload.request_metadata.request_attributes.protocol: value is required [required]", err.Error()) + assert.Nil(t, cloudEvent) + assert.Nil(t, routingIdentifier) + assert.Equal(t, "", op) + }) + t.Run("required only", func(t *testing.T) { api, _ := NewMockAuditApi() sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator() @@ -696,6 +836,121 @@ func Test_AuditEventBuilder(t *testing.T) { assert.NoError(t, err) }) + t.Run("system event", func(t *testing.T) { + api, _ := NewMockAuditApi() + sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator() + tracer := otel.Tracer("test") + + objectId := uuid.NewString() + operation := "stackit.demo-service.v1.operation" + routableIdentifier := RoutableIdentifier{Identifier: objectId, Type: SingularTypeProject} + builder := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", "worker-id", "eu01"). + WithRequiredObjectId(objectId). + WithRequiredObjectType(SingularTypeProject). + WithRequiredOperation(operation). + AsSystemEvent() + + cloudEvent, routingIdentifier, op, err := builder.Build(context.Background(), SequenceNumber(1)) + assert.NoError(t, err) + assert.True(t, builder.IsBuilt()) + + assert.Equal(t, &routableIdentifier, routingIdentifier) + assert.Equal(t, operation, op) + + assert.NotNil(t, cloudEvent) + assert.Equal(t, "application/cloudevents+protobuf", cloudEvent.DataContentType) + assert.Equal(t, "audit.v1.RoutableAuditEvent", cloudEvent.DataType) + assert.Regexp(t, "[0-9]+/eu01/worker-id/1", cloudEvent.Id) + assert.Equal(t, "demo-service", cloudEvent.Source) + assert.Equal(t, "1.0", cloudEvent.SpecVersion) + assert.Equal(t, fmt.Sprintf("projects/%s", objectId), cloudEvent.Subject) + assert.NotNil(t, cloudEvent.Time) + assert.Equal(t, "00-00000000000000000000000000000000-0000000000000000-00", *cloudEvent.TraceParent) + assert.Nil(t, cloudEvent.TraceState) + + var routableAuditEvent auditV1.RoutableAuditEvent + assert.NotNil(t, cloudEvent.Data) + assert.NoError(t, proto.Unmarshal(cloudEvent.Data, &routableAuditEvent)) + + assert.Equal(t, routableIdentifier.ToObjectIdentifier(), routableAuditEvent.ObjectIdentifier) + assert.Equal(t, auditV1.Visibility_VISIBILITY_PRIVATE, routableAuditEvent.Visibility) + assert.Equal(t, operation, routableAuditEvent.OperationName) + + var logEntry auditV1.AuditLogEntry + assert.NotNil(t, routableAuditEvent.GetUnencryptedData().Data) + assert.NoError(t, proto.Unmarshal(routableAuditEvent.GetUnencryptedData().Data, &logEntry)) + + assert.Equal(t, fmt.Sprintf("projects/%s/logs/system-event", objectId), logEntry.LogName) + assert.Nil(t, logEntry.Labels) + assert.Nil(t, logEntry.TraceState) + assert.Nil(t, logEntry.TraceParent) + assert.Equal(t, auditV1.LogSeverity_LOG_SEVERITY_DEFAULT, logEntry.Severity) + assert.NotNil(t, logEntry.Timestamp) + assert.Nil(t, logEntry.CorrelationId) + assert.Regexp(t, "[0-9]+/eu01/worker-id/1", logEntry.InsertId) + + assert.NotNil(t, logEntry.ProtoPayload) + + authenticationInfo := logEntry.ProtoPayload.AuthenticationInfo + assert.NotNil(t, authenticationInfo) + assert.Equal(t, "do-not-reply@stackit.cloud", authenticationInfo.PrincipalEmail) + assert.Equal(t, "none", authenticationInfo.PrincipalId) + assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo) + assert.Nil(t, authenticationInfo.ServiceAccountName) + + assert.Nil(t, logEntry.ProtoPayload.AuthorizationInfo) + assert.Nil(t, logEntry.ProtoPayload.Metadata) + assert.Equal(t, operation, logEntry.ProtoPayload.OperationName) + assert.Nil(t, logEntry.ProtoPayload.Request) + + requestMetadata := logEntry.ProtoPayload.RequestMetadata + assert.NotNil(t, requestMetadata) + assert.Equal(t, "0.0.0.0", requestMetadata.CallerIp) + assert.Equal(t, "none", requestMetadata.CallerSuppliedUserAgent) + + requestAttributes := requestMetadata.RequestAttributes + assert.NotNil(t, requestAttributes) + assert.Equal(t, "none", requestAttributes.Path) + assert.NotNil(t, requestAttributes.Time) + assert.Equal(t, "0.0.0.0", requestAttributes.Host) + assert.Equal(t, auditV1.AttributeContext_HTTP_METHOD_OTHER, requestAttributes.Method) + assert.Nil(t, requestAttributes.Id) + assert.Equal(t, "none", requestAttributes.Scheme) + assert.Equal(t, map[string]string{"user-agent": "none"}, requestAttributes.Headers) + assert.Nil(t, requestAttributes.Query) + assert.Equal(t, "none", requestAttributes.Protocol) + + requestAttributesAuth := requestAttributes.Auth + assert.NotNil(t, requestAttributesAuth) + assert.Equal(t, "none/none", requestAttributesAuth.Principal) + assert.Nil(t, requestAttributesAuth.Audiences) + assert.NotNil(t, requestAttributesAuth.Claims) + assert.Equal(t, map[string]any{}, requestAttributesAuth.Claims.AsMap()) + + assert.Equal(t, fmt.Sprintf("projects/%s", objectId), logEntry.ProtoPayload.ResourceName) + assert.Nil(t, logEntry.ProtoPayload.Response) + + responseMetadata := logEntry.ProtoPayload.ResponseMetadata + assert.NotNil(t, responseMetadata) + assert.Nil(t, responseMetadata.ErrorDetails) + assert.Nil(t, responseMetadata.ErrorMessage) + assert.Equal(t, wrapperspb.Int32(200), responseMetadata.StatusCode) + + responseAttributes := responseMetadata.ResponseAttributes + assert.NotNil(t, responseAttributes) + assert.Nil(t, responseAttributes.Headers) + assert.Nil(t, responseAttributes.NumResponseItems) + assert.Nil(t, responseAttributes.Size) + assert.NotNil(t, responseAttributes.Time) + + assert.Equal(t, "demo-service", logEntry.ProtoPayload.ServiceName) + + validator, err := protovalidate.New() + assert.NoError(t, err) + err = validator.Validate(&logEntry) + assert.NoError(t, err) + }) + t.Run("with responsebody unserialized", func(t *testing.T) { api, _ := NewMockAuditApi() sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator() diff --git a/audit/api/model.go b/audit/api/model.go index f875623..aa83d36 100644 --- a/audit/api/model.go +++ b/audit/api/model.go @@ -657,9 +657,10 @@ func AuditAttributesFromAuthorizationHeader(request *ApiRequest) ( error, ) { - var principalId string - var principalEmail string - var auditClaims *structpb.Struct = nil + var principalId = "none" + var principalEmail = "do-not-reply@stackit.cloud" + emptyClaims, _ := structpb.NewStruct(make(map[string]interface{})) + var auditClaims = emptyClaims var authenticationPrincipal = "none/none" var serviceAccountName *string = nil audiences := make([]string, 0)