Add optional response body serialization to event builder

This commit is contained in:
Christian Schaible 2024-10-01 12:17:36 +02:00
parent 0caeabedbd
commit a364d42c7d
2 changed files with 218 additions and 2 deletions

View file

@ -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

View file

@ -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()