diff --git a/audit/api/builder.go b/audit/api/builder.go index 176789d..1709ff2 100644 --- a/audit/api/builder.go +++ b/audit/api/builder.go @@ -31,6 +31,8 @@ type AuditParameters struct { // Type of the object, the audit event refers to ObjectType SingularType + ResponseBody any + // Log severity Severity auditV1.LogSeverity } @@ -256,7 +258,13 @@ func (builder *AuditLogEntryBuilder) WithStatusCode(statusCode int) *AuditLogEnt return builder } -// WithResponseBodyBytes adds the response body as bytes (json or protobuf expected) +// WithResponseBody adds the response body to the builder and transforms it in the Build method (json serializable or protobuf message expected) +func (builder *AuditLogEntryBuilder) WithResponseBody(responseBody any) *AuditLogEntryBuilder { + builder.auditParams.ResponseBody = responseBody + return builder +} + +// WithResponseBodyBytes adds the response body as bytes (serialized json or protobuf message expected) func (builder *AuditLogEntryBuilder) WithResponseBodyBytes(responseBody *[]byte) *AuditLogEntryBuilder { builder.auditResponse.ResponseBodyBytes = responseBody return builder @@ -292,6 +300,16 @@ func (builder *AuditLogEntryBuilder) Build(ctx context.Context, sequenceNumber S return nil, err } + if builder.auditResponse.ResponseBodyBytes != nil && builder.auditParams.ResponseBody != nil { + return nil, errors.New("responseBodyBytes and responseBody set") + } else if builder.auditParams.ResponseBody != nil { + responseBytes, err := ResponseBodyToBytes(builder.auditParams.ResponseBody) + if err != nil { + return nil, err + } + builder.auditResponse.ResponseBodyBytes = responseBytes + } + resourceName := fmt.Sprintf("%s/%s", *pluralType, objectId) builder.auditMetadata.AuditInsertId = NewInsertId(time.Now().UTC(), builder.location, builder.workerId, uint64(sequenceNumber)) builder.auditMetadata.AuditLogName = fmt.Sprintf("%s/%s/logs/%s", *pluralType, objectId, builder.auditParams.EventType) @@ -505,7 +523,13 @@ func (builder *AuditEventBuilder) WithStatusCode(statusCode int) *AuditEventBuil return builder } -// WithResponseBodyBytes adds the response body as bytes (json or protobuf expected) +// WithResponseBody adds the response body to the builder and transforms it in the Build method (json serializable or protobuf message expected) +func (builder *AuditEventBuilder) WithResponseBody(responseBody any) *AuditEventBuilder { + builder.auditLogEntryBuilder.WithResponseBody(responseBody) + return builder +} + +// WithResponseBodyBytes adds the response body as bytes (serialized json or protobuf message expected) func (builder *AuditEventBuilder) WithResponseBodyBytes(responseBody *[]byte) *AuditEventBuilder { builder.auditLogEntryBuilder.WithResponseBodyBytes(responseBody) return builder diff --git a/audit/api/builder_test.go b/audit/api/builder_test.go index 86a00c9..8feab54 100644 --- a/audit/api/builder_test.go +++ b/audit/api/builder_test.go @@ -291,6 +291,123 @@ func Test_AuditLogEntryBuilder(t *testing.T) { 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" + permissionCheckResult := true + requestTime := time.Now().AddDate(0, 0, -2).UTC() + responseTime := time.Now().AddDate(0, 0, -1).UTC() + responseBody := map[string]interface{}{"key": "response"} + builder := NewAuditLogEntryBuilder(). + WithRequiredLocation("eu01"). + WithRequiredObjectId("1"). + WithRequiredObjectType(SingularTypeProject). + WithRequiredOperation("stackit.demo-service.v1.operation"). + WithRequiredApiRequest(ApiRequest{ + Body: nil, + Header: map[string][]string{"user-agent": {"custom"}, "authorization": {"Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOGJlZjc1LWRmY2QtNGE3My1hMzkxLTU0YTdhZjU3YTdkNiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic3RhY2tpdC1wb3J0YWwtbG9naW4tZGV2LWNsaWVudC1pZCJdLCJjbGllbnRfaWQiOiJzdGFja2l0LXBvcnRhbC1sb2dpbi1kZXYtY2xpZW50LWlkIiwiZW1haWwiOiJDaHJpc3RpYW4uU2NoYWlibGVAbm92YXRlYy1nbWJoLmRlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6MTcyMjU5MDM2NywiaWF0IjoxNzIyNTg2NzY3LCJpc3MiOiJodHRwczovL2FjY291bnRzLmRldi5zdGFja2l0LmNsb3VkIiwianRpIjoiZDczYTY3YWMtZDFlYy00YjU1LTk5ZDQtZTk1MzI3NWYwMjJhIiwibmJmIjoxNzIyNTg2NzY3LCJzY29wZSI6Im9wZW5pZCBlbWFpbCIsInN1YiI6ImNkOTRmMDFhLWRmMmUtNDQ1Ni05MDJlLTQ4ZjVlNTdmMGI2MyJ9.ajhjYbC5l5g7un9NSheoAwBT83YcZM91rH4DJxPTDsB78HzIVrmaKTPrK3AI_E1THlD2Z3_ot9nFr_eX7XcwWp_ZBlataKmakdXlAmeb4xSMGNYefIfzV_3w9ZZAZ66yoeTrtn8dUx5ezquenCYpctB1NcccmK4U09V0kNcq9dFcfF3Sg9YilF3orUCR0ql1d9RnOs3EiFZuUpdBEkyoVsAdSh2P-PRbNViR_FgCcAJem97TsN5CQc9RlvKYe4sYKgqQoqa2GDVi9Niiw3fe1V8SCnROYcpkOzBBWdvuzFMBUjln3uOogYVOz93xkmImV6jidgyQ70fLt-eDUmZZfg"}}, + Host: "localhost", + Method: "POST", + Scheme: "https", + Proto: "HTTP/1.1", + URL: RequestUrl{ + Path: "/", + RawQuery: nil, + }, + }). + WithRequiredRequestClientIp("127.0.0.1"). + WithRequiredServiceName("demo-service"). + WithRequiredWorkerId("worker-id"). + WithAuditPermission(permission). + WithAuditPermissionCheckResult(permissionCheckResult). + WithDetails(details). + WithEventType(EventTypeSystemEvent). + WithLabels(map[string]string{"key": "label"}). + WithNumResponseItems(int64(10)). + WithRequestCorrelationId("correlationId"). + WithRequestId("requestId"). + WithRequestTime(requestTime). + WithResponseBody(responseBody). + WithResponseHeaders(map[string][]string{"key": {"header"}}). + WithResponseTime(responseTime). + WithSeverity(auditV1.LogSeverity_LOG_SEVERITY_ERROR). + WithStatusCode(400) + + logEntry, err := builder.Build(context.Background(), SequenceNumber(1)) + assert.NoError(t, err) + assert.NotNil(t, logEntry) + assert.NotNil(t, logEntry.ProtoPayload) + + expectedResponse, _ := structpb.NewStruct(responseBody) + assert.Equal(t, "projects/1", logEntry.ProtoPayload.ResourceName) + assert.Equal(t, expectedResponse, logEntry.ProtoPayload.Response) + + validator, err := protovalidate.New() + assert.NoError(t, err) + err = validator.Validate(logEntry) + assert.NoError(t, err) + }) + + t.Run("with response body and response body bytes set", func(t *testing.T) { + responseBody := map[string]interface{}{"key": "response"} + responseBodyBytes, err := ResponseBodyToBytes(responseBody) + assert.NoError(t, err) + builder := NewAuditLogEntryBuilder(). + WithRequiredApiRequest(ApiRequest{ + Body: nil, + Header: map[string][]string{"user-agent": {"custom"}, "authorization": {"Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOGJlZjc1LWRmY2QtNGE3My1hMzkxLTU0YTdhZjU3YTdkNiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic3RhY2tpdC1wb3J0YWwtbG9naW4tZGV2LWNsaWVudC1pZCJdLCJjbGllbnRfaWQiOiJzdGFja2l0LXBvcnRhbC1sb2dpbi1kZXYtY2xpZW50LWlkIiwiZW1haWwiOiJDaHJpc3RpYW4uU2NoYWlibGVAbm92YXRlYy1nbWJoLmRlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6MTcyMjU5MDM2NywiaWF0IjoxNzIyNTg2NzY3LCJpc3MiOiJodHRwczovL2FjY291bnRzLmRldi5zdGFja2l0LmNsb3VkIiwianRpIjoiZDczYTY3YWMtZDFlYy00YjU1LTk5ZDQtZTk1MzI3NWYwMjJhIiwibmJmIjoxNzIyNTg2NzY3LCJzY29wZSI6Im9wZW5pZCBlbWFpbCIsInN1YiI6ImNkOTRmMDFhLWRmMmUtNDQ1Ni05MDJlLTQ4ZjVlNTdmMGI2MyJ9.ajhjYbC5l5g7un9NSheoAwBT83YcZM91rH4DJxPTDsB78HzIVrmaKTPrK3AI_E1THlD2Z3_ot9nFr_eX7XcwWp_ZBlataKmakdXlAmeb4xSMGNYefIfzV_3w9ZZAZ66yoeTrtn8dUx5ezquenCYpctB1NcccmK4U09V0kNcq9dFcfF3Sg9YilF3orUCR0ql1d9RnOs3EiFZuUpdBEkyoVsAdSh2P-PRbNViR_FgCcAJem97TsN5CQc9RlvKYe4sYKgqQoqa2GDVi9Niiw3fe1V8SCnROYcpkOzBBWdvuzFMBUjln3uOogYVOz93xkmImV6jidgyQ70fLt-eDUmZZfg"}}, + Host: "localhost", + Method: "POST", + Scheme: "https", + Proto: "HTTP/1.1", + URL: RequestUrl{ + Path: "/", + RawQuery: nil, + }, + }). + WithRequiredLocation("eu01"). + WithRequiredObjectId("1"). + WithRequiredObjectType(SingularTypeProject). + WithRequiredOperation("stackit.demo-service.v1.operation"). + WithRequiredRequestClientIp("127.0.0.1"). + WithRequiredServiceName("demo-service"). + WithRequiredWorkerId("worker-id"). + WithResponseBody(responseBody). + WithResponseBodyBytes(responseBodyBytes) + + logEntry, err := builder.Build(context.Background(), SequenceNumber(1)) + assert.EqualError(t, err, "responseBodyBytes and responseBody set") + assert.Nil(t, logEntry) + }) + + t.Run("with invalid response body", func(t *testing.T) { + builder := NewAuditLogEntryBuilder(). + WithRequiredApiRequest(ApiRequest{ + Body: nil, + Header: map[string][]string{"user-agent": {"custom"}, "authorization": {"Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOGJlZjc1LWRmY2QtNGE3My1hMzkxLTU0YTdhZjU3YTdkNiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic3RhY2tpdC1wb3J0YWwtbG9naW4tZGV2LWNsaWVudC1pZCJdLCJjbGllbnRfaWQiOiJzdGFja2l0LXBvcnRhbC1sb2dpbi1kZXYtY2xpZW50LWlkIiwiZW1haWwiOiJDaHJpc3RpYW4uU2NoYWlibGVAbm92YXRlYy1nbWJoLmRlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6MTcyMjU5MDM2NywiaWF0IjoxNzIyNTg2NzY3LCJpc3MiOiJodHRwczovL2FjY291bnRzLmRldi5zdGFja2l0LmNsb3VkIiwianRpIjoiZDczYTY3YWMtZDFlYy00YjU1LTk5ZDQtZTk1MzI3NWYwMjJhIiwibmJmIjoxNzIyNTg2NzY3LCJzY29wZSI6Im9wZW5pZCBlbWFpbCIsInN1YiI6ImNkOTRmMDFhLWRmMmUtNDQ1Ni05MDJlLTQ4ZjVlNTdmMGI2MyJ9.ajhjYbC5l5g7un9NSheoAwBT83YcZM91rH4DJxPTDsB78HzIVrmaKTPrK3AI_E1THlD2Z3_ot9nFr_eX7XcwWp_ZBlataKmakdXlAmeb4xSMGNYefIfzV_3w9ZZAZ66yoeTrtn8dUx5ezquenCYpctB1NcccmK4U09V0kNcq9dFcfF3Sg9YilF3orUCR0ql1d9RnOs3EiFZuUpdBEkyoVsAdSh2P-PRbNViR_FgCcAJem97TsN5CQc9RlvKYe4sYKgqQoqa2GDVi9Niiw3fe1V8SCnROYcpkOzBBWdvuzFMBUjln3uOogYVOz93xkmImV6jidgyQ70fLt-eDUmZZfg"}}, + Host: "localhost", + Method: "POST", + Scheme: "https", + Proto: "HTTP/1.1", + URL: RequestUrl{ + Path: "/", + RawQuery: nil, + }, + }). + WithRequiredLocation("eu01"). + WithRequiredObjectId("1"). + WithRequiredObjectType(SingularTypeProject). + WithRequiredOperation("stackit.demo-service.v1.operation"). + WithRequiredRequestClientIp("127.0.0.1"). + WithRequiredServiceName("demo-service"). + WithRequiredWorkerId("worker-id"). + WithResponseBody("invalid") + + logEntry, err := builder.Build(context.Background(), SequenceNumber(1)) + assert.EqualError(t, err, "json: cannot unmarshal string into Go value of type map[string]interface {}\ninvalid response") + assert.Nil(t, logEntry) + }) } func Test_AuditEventBuilder(t *testing.T) { @@ -579,6 +696,81 @@ func Test_AuditEventBuilder(t *testing.T) { assert.NoError(t, err) }) + t.Run("with responsebody unserialized", func(t *testing.T) { + api, _ := NewMockAuditApi() + sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator() + tracer := otel.Tracer("test") + + objectId := uuid.NewString() + operation := "stackit.demo-service.v1.operation" + details := map[string]interface{}{"key": "detail"} + permission := "project.edit" + permissionCheckResult := true + requestTime := time.Now().AddDate(0, 0, -2).UTC() + responseTime := time.Now().AddDate(0, 0, -1).UTC() + responseBody := map[string]interface{}{"key": "response"} + builder := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", "worker-id", "eu01"). + WithRequiredObjectId(objectId). + WithRequiredObjectType(SingularTypeProject). + WithRequiredOperation(operation). + WithRequiredApiRequest(ApiRequest{ + Body: nil, + Header: map[string][]string{"user-agent": {"custom"}, "authorization": {"Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOGJlZjc1LWRmY2QtNGE3My1hMzkxLTU0YTdhZjU3YTdkNiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic3RhY2tpdC1wb3J0YWwtbG9naW4tZGV2LWNsaWVudC1pZCJdLCJjbGllbnRfaWQiOiJzdGFja2l0LXBvcnRhbC1sb2dpbi1kZXYtY2xpZW50LWlkIiwiZW1haWwiOiJDaHJpc3RpYW4uU2NoYWlibGVAbm92YXRlYy1nbWJoLmRlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6MTcyMjU5MDM2NywiaWF0IjoxNzIyNTg2NzY3LCJpc3MiOiJodHRwczovL2FjY291bnRzLmRldi5zdGFja2l0LmNsb3VkIiwianRpIjoiZDczYTY3YWMtZDFlYy00YjU1LTk5ZDQtZTk1MzI3NWYwMjJhIiwibmJmIjoxNzIyNTg2NzY3LCJzY29wZSI6Im9wZW5pZCBlbWFpbCIsInN1YiI6ImNkOTRmMDFhLWRmMmUtNDQ1Ni05MDJlLTQ4ZjVlNTdmMGI2MyJ9.ajhjYbC5l5g7un9NSheoAwBT83YcZM91rH4DJxPTDsB78HzIVrmaKTPrK3AI_E1THlD2Z3_ot9nFr_eX7XcwWp_ZBlataKmakdXlAmeb4xSMGNYefIfzV_3w9ZZAZ66yoeTrtn8dUx5ezquenCYpctB1NcccmK4U09V0kNcq9dFcfF3Sg9YilF3orUCR0ql1d9RnOs3EiFZuUpdBEkyoVsAdSh2P-PRbNViR_FgCcAJem97TsN5CQc9RlvKYe4sYKgqQoqa2GDVi9Niiw3fe1V8SCnROYcpkOzBBWdvuzFMBUjln3uOogYVOz93xkmImV6jidgyQ70fLt-eDUmZZfg"}}, + Host: "localhost", + Method: "POST", + Scheme: "https", + Proto: "HTTP/1.1", + URL: RequestUrl{ + Path: "/", + RawQuery: nil, + }, + }). + WithRequiredRequestClientIp("127.0.0.1"). + WithAuditPermission(permission). + WithAuditPermissionCheckResult(permissionCheckResult). + WithDetails(details). + WithEventType(EventTypeSystemEvent). + WithLabels(map[string]string{"key": "label"}). + WithNumResponseItems(int64(10)). + WithRequestCorrelationId("correlationId"). + WithRequestId("requestId"). + WithRequestTime(requestTime). + WithResponseBody(responseBody). + WithResponseHeaders(map[string][]string{"key": {"header"}}). + WithResponseTime(responseTime). + WithSeverity(auditV1.LogSeverity_LOG_SEVERITY_ERROR). + WithStatusCode(400). + WithVisibility(auditV1.Visibility_VISIBILITY_PRIVATE) + + routableIdentifier := RoutableIdentifier{Identifier: objectId, Type: SingularTypeProject} + + 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) + + var routableAuditEvent auditV1.RoutableAuditEvent + assert.NotNil(t, cloudEvent.Data) + assert.NoError(t, proto.Unmarshal(cloudEvent.Data, &routableAuditEvent)) + + var logEntry auditV1.AuditLogEntry + assert.NotNil(t, routableAuditEvent.GetUnencryptedData().Data) + assert.NoError(t, proto.Unmarshal(routableAuditEvent.GetUnencryptedData().Data, &logEntry)) + assert.NotNil(t, logEntry.ProtoPayload) + + expectedResponse, _ := structpb.NewStruct(responseBody) + assert.Equal(t, fmt.Sprintf("projects/%s", objectId), logEntry.ProtoPayload.ResourceName) + assert.Equal(t, expectedResponse, logEntry.ProtoPayload.Response) + + validator, err := protovalidate.New() + assert.NoError(t, err) + err = validator.Validate(&logEntry) + assert.NoError(t, err) + }) + t.Run("no entry builder", func(t *testing.T) { api, _ := NewMockAuditApi() sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator()