Add event builder

This commit is contained in:
Christian Schaible 2024-09-30 13:08:43 +02:00
parent ab40008fb1
commit f8f0b48437
4 changed files with 1291 additions and 0 deletions

584
audit/api/builder.go Normal file
View file

@ -0,0 +1,584 @@
package api
import (
"context"
"dev.azure.com/schwarzit/schwarzit.stackit-core-platform/audit-go.git/audit/utils"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/audit-go.git/gen/go/audit/v1"
"errors"
"fmt"
"go.opentelemetry.io/otel/trace"
"log/slog"
"time"
)
type SequenceNumber uint64
type AuditParameters struct {
// A map that is added as "details" to the message
Details map[string]interface{}
// The type of the event
EventType EventType
// A set of user-defined (key, value) data that provides additional
// information about the log entry.
Labels map[string]string
// UUID identifier of the object, the audit event refers to
ObjectId string
// Type of the object, the audit event refers to
ObjectType SingularType
// Log severity
Severity auditV1.LogSeverity
}
func getObjectIdAndTypeFromAuditParams(
ctx context.Context,
auditParams *AuditParameters,
) (string, *SingularType, *PluralType, error) {
objectId := auditParams.ObjectId
if objectId == "" {
return "", nil, nil, errors.New("object id missing")
}
var objectType *SingularType
if auditParams.ObjectType != "" {
objectType = &auditParams.ObjectType
}
if objectType == nil {
return "", nil, nil, errors.New("singular type missing")
}
// Convert to plural type
plural, err := objectType.AsPluralType()
if err != nil {
slog.LogAttrs(ctx, slog.LevelError, "failed to convert singular type to plural type", slog.Any("error", err))
return "", nil, nil, err
}
return objectId, objectType, &plural, nil
}
// AuditLogEntryBuilder collects audit params to construct auditV1.AuditLogEntry
type AuditLogEntryBuilder struct {
auditParams AuditParameters
auditRequest AuditRequest
auditResponse AuditResponse
auditMetadata AuditMetadata
// Region and optional zone id. If both, separated with a - (dash).
// Example: eu01
location string
// The ID of the K8s Pod, Service-Instance, etc (must be unique for a sending service)
workerId string
}
// NewAuditLogEntryBuilder returns a builder to construct auditV1.AuditLogEntry
func NewAuditLogEntryBuilder() *AuditLogEntryBuilder {
requestTime := time.Now().UTC()
return &AuditLogEntryBuilder{
auditParams: AuditParameters{
EventType: EventTypeAdminActivity,
},
auditRequest: AuditRequest{
Request: nil,
RequestClientIP: "0.0.0.0",
RequestCorrelationId: nil,
RequestId: nil,
RequestTime: &requestTime,
},
auditResponse: AuditResponse{
ResponseBodyBytes: nil,
ResponseStatusCode: 200,
ResponseHeaders: make(map[string][]string),
ResponseNumItems: nil,
ResponseTime: nil,
},
auditMetadata: AuditMetadata{
AuditInsertId: "",
AuditLabels: nil,
AuditLogName: "",
AuditLogSeverity: auditV1.LogSeverity_LOG_SEVERITY_DEFAULT,
AuditOperationName: "",
AuditPermission: nil,
AuditPermissionGranted: nil,
AuditResourceName: "",
AuditServiceName: "",
AuditTime: nil,
},
location: "",
workerId: "",
}
}
// WithRequiredApiRequest adds api request details
func (builder *AuditLogEntryBuilder) WithRequiredApiRequest(request ApiRequest) *AuditLogEntryBuilder {
builder.auditRequest.Request = &request
return builder
}
// WithRequiredLocation adds the region and optional zone id. If both, separated with a - (dash).
// Example: eu01
func (builder *AuditLogEntryBuilder) WithRequiredLocation(location string) *AuditLogEntryBuilder {
builder.location = location
return builder
}
// WithRequiredRequestClientIp adds the client ip
func (builder *AuditLogEntryBuilder) WithRequiredRequestClientIp(requestClientIp string) *AuditLogEntryBuilder {
builder.auditRequest.RequestClientIP = requestClientIp
return builder
}
// WithRequestCorrelationId adds an optional request correlation id
func (builder *AuditLogEntryBuilder) WithRequestCorrelationId(requestCorrelationId string) *AuditLogEntryBuilder {
builder.auditRequest.RequestCorrelationId = &requestCorrelationId
return builder
}
// WithRequestId adds an optional request id
func (builder *AuditLogEntryBuilder) WithRequestId(requestId string) *AuditLogEntryBuilder {
builder.auditRequest.RequestId = &requestId
return builder
}
// WithRequestTime sets the request time on the builder. If not set - the instantiation time of the builder is used.
func (builder *AuditLogEntryBuilder) WithRequestTime(requestTime time.Time) *AuditLogEntryBuilder {
builder.auditRequest.RequestTime = &requestTime
return builder
}
// WithRequiredServiceName adds the service name in lowercase (allowed characters are [a-z-]).
func (builder *AuditLogEntryBuilder) WithRequiredServiceName(serviceName string) *AuditLogEntryBuilder {
builder.auditMetadata.AuditServiceName = serviceName
return builder
}
// WithRequiredWorkerId adds the ID of the K8s Pod, Service-Instance, etc. (must be unique for a sending service)
func (builder *AuditLogEntryBuilder) WithRequiredWorkerId(workerId string) *AuditLogEntryBuilder {
builder.workerId = workerId
return builder
}
// WithRequiredObjectId adds the object identifier.
// May be prefilled by audit middleware (if the identifier can be extracted from the url path).
func (builder *AuditLogEntryBuilder) WithRequiredObjectId(objectId string) *AuditLogEntryBuilder {
builder.auditParams.ObjectId = objectId
return builder
}
// WithRequiredObjectType adds the object type.
// May be prefilled by audit middleware (if the type can be extracted from the url path).
func (builder *AuditLogEntryBuilder) WithRequiredObjectType(objectType SingularType) *AuditLogEntryBuilder {
builder.auditParams.ObjectType = objectType
return builder
}
// WithRequiredOperation adds the name of the service method or operation.
//
// Format: stackit.<product>.<version>.<type-chain>.<operation>
// Where:
//
// Product: The name of the service in lowercase
// Version: Optional API version
// Type-Chain: Chained path to object
// Operation: The name of the operation in lowercase
//
// Examples:
//
// "stackit.resource-manager.v1.organizations.create"
// "stackit.authorization.v1.projects.volumes.create"
// "stackit.authorization.v2alpha.projects.volumes.create"
// "stackit.authorization.v2.folders.move"
// "stackit.resource-manager.health"
func (builder *AuditLogEntryBuilder) WithRequiredOperation(operation string) *AuditLogEntryBuilder {
builder.auditMetadata.AuditOperationName = operation
return builder
}
// WithAuditPermission adds the IAM permission
//
// Examples:
//
// "resourcemanager.project.edit"
func (builder *AuditLogEntryBuilder) WithAuditPermission(permission string) *AuditLogEntryBuilder {
builder.auditMetadata.AuditPermission = &permission
return builder
}
// WithAuditPermissionCheckResult adds the IAM permission check result
func (builder *AuditLogEntryBuilder) WithAuditPermissionCheckResult(permissionCheckResult bool) *AuditLogEntryBuilder {
builder.auditMetadata.AuditPermissionGranted = &permissionCheckResult
return builder
}
// WithLabels adds A set of user-defined (key, value) data that provides additional
// information about the log entry.
func (builder *AuditLogEntryBuilder) WithLabels(labels map[string]string) *AuditLogEntryBuilder {
builder.auditMetadata.AuditLabels = &labels
return builder
}
// WithNumResponseItems adds the number of items returned to the client if applicable.
func (builder *AuditLogEntryBuilder) WithNumResponseItems(numResponseItems int64) *AuditLogEntryBuilder {
builder.auditResponse.ResponseNumItems = &numResponseItems
return builder
}
// WithEventType overwrites the default event type EventTypeAdminActivity
func (builder *AuditLogEntryBuilder) WithEventType(eventType EventType) *AuditLogEntryBuilder {
builder.auditParams.EventType = eventType
return builder
}
// WithDetails adds an optional details object to the audit log entry
func (builder *AuditLogEntryBuilder) WithDetails(details map[string]interface{}) *AuditLogEntryBuilder {
builder.auditParams.Details = details
return builder
}
// WithSeverity overwrites the default log severity level auditV1.LogSeverity_LOG_SEVERITY_DEFAULT
func (builder *AuditLogEntryBuilder) WithSeverity(severity auditV1.LogSeverity) *AuditLogEntryBuilder {
builder.auditMetadata.AuditLogSeverity = severity
return builder
}
// WithStatusCode adds the (http) response status code
func (builder *AuditLogEntryBuilder) WithStatusCode(statusCode int) *AuditLogEntryBuilder {
builder.auditResponse.ResponseStatusCode = statusCode
return builder
}
// WithResponseBodyBytes adds the response body as bytes (json or protobuf expected)
func (builder *AuditLogEntryBuilder) WithResponseBodyBytes(responseBody *[]byte) *AuditLogEntryBuilder {
builder.auditResponse.ResponseBodyBytes = responseBody
return builder
}
// WithResponseHeaders adds response headers
func (builder *AuditLogEntryBuilder) WithResponseHeaders(responseHeaders map[string][]string) *AuditLogEntryBuilder {
builder.auditResponse.ResponseHeaders = responseHeaders
return builder
}
// WithResponseTime adds the time when the response is sent
func (builder *AuditLogEntryBuilder) WithResponseTime(responseTime time.Time) *AuditLogEntryBuilder {
builder.auditResponse.ResponseTime = &responseTime
return builder
}
// Build constructs the auditV1.AuditLogEntry.
//
// Parameters:
// - A context object
// - A SequenceNumber
//
// Returns:
// - The auditV1.AuditLogEntry protobuf message or
// - Error if the entry cannot be built
func (builder *AuditLogEntryBuilder) Build(ctx context.Context, sequenceNumber SequenceNumber) (*auditV1.AuditLogEntry, error) {
auditTime := time.Now()
builder.auditMetadata.AuditTime = &auditTime
objectId, _, pluralType, err := getObjectIdAndTypeFromAuditParams(ctx, &builder.auditParams)
if err != nil {
return nil, err
}
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)
builder.auditMetadata.AuditResourceName = resourceName
var details *map[string]interface{} = nil
if len(builder.auditParams.Details) > 0 {
details = &builder.auditParams.Details
}
// Instantiate the audit event
return NewAuditLogEntry(
builder.auditRequest,
builder.auditResponse,
details,
builder.auditMetadata,
nil,
nil,
)
}
// AuditEventBuilder collects audit log parameters, validates input and
// returns a cloud event that can be sent to the audit log system.
type AuditEventBuilder struct {
// The audit api used to validate, serialize and send events
api *AuditApi
// The audit log entry builder which is used to build the actual protobuf message
auditLogEntryBuilder *AuditLogEntryBuilder
// Status whether the event has been built
built bool
// Sequence number generator providing sequential increasing numbers for the insert IDs
sequenceNumberGenerator *utils.SequenceNumberGenerator
// Opentelemtry tracer
tracer trace.Tracer
// Visibility of the event
visibility auditV1.Visibility
}
// NewAuditEventBuilder returns a builder that collects audit log parameters,
// validates input and returns a cloud event that can be sent to the audit log system.
func NewAuditEventBuilder(
// The audit api used to validate, serialize and send events
api *AuditApi,
// The sequence number generator can be used to get and revert sequence numbers to build audit log events
sequenceNumberGenerator *utils.SequenceNumberGenerator,
// Tracer
tracer trace.Tracer,
// The service name in lowercase (allowed characters are [a-z-]).
serviceName string,
// The ID of the K8s Pod, Service-Instance, etc. (must be unique for a sending service)
workerId string,
// The location of the service (e.g. eu01)
location string,
) *AuditEventBuilder {
return &AuditEventBuilder{
api: api,
auditLogEntryBuilder: NewAuditLogEntryBuilder().
WithRequiredServiceName(serviceName).
WithRequiredWorkerId(workerId).
WithRequiredLocation(location),
sequenceNumberGenerator: sequenceNumberGenerator,
tracer: tracer,
visibility: auditV1.Visibility_VISIBILITY_PUBLIC,
}
}
// NextSequenceNumber returns the next sequence number from utils.SequenceNumberGenerator.
// In case of an error RevertSequenceNumber must be called to prevent gaps in the sequence of numbers.
func (builder *AuditEventBuilder) NextSequenceNumber() SequenceNumber {
return SequenceNumber((*builder.sequenceNumberGenerator).Next())
}
// RevertSequenceNumber can be called to decrease the sequence number on the utils.SequenceNumberGenerator in case of an error
func (builder *AuditEventBuilder) RevertSequenceNumber() {
(*builder.sequenceNumberGenerator).Revert()
}
// WithAuditLogEntryBuilder overwrites the preconfigured AuditLogEntryBuilder
func (builder *AuditEventBuilder) WithAuditLogEntryBuilder(auditLogEntryBuilder *AuditLogEntryBuilder) *AuditEventBuilder {
builder.auditLogEntryBuilder = auditLogEntryBuilder
return builder
}
// WithRequiredApiRequest adds api request details
func (builder *AuditEventBuilder) WithRequiredApiRequest(request ApiRequest) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequiredApiRequest(request)
return builder
}
// WithRequiredRequestClientIp adds the client ip
func (builder *AuditEventBuilder) WithRequiredRequestClientIp(requestClientIp string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequiredRequestClientIp(requestClientIp)
return builder
}
// WithRequestCorrelationId adds an optional request correlation id
func (builder *AuditEventBuilder) WithRequestCorrelationId(requestCorrelationId string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequestCorrelationId(requestCorrelationId)
return builder
}
// WithRequestId adds an optional request id
func (builder *AuditEventBuilder) WithRequestId(requestId string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequestId(requestId)
return builder
}
// WithRequestTime sets the request time on the builder. If not set - the instantiation time of the builder is used.
func (builder *AuditEventBuilder) WithRequestTime(requestTime time.Time) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequestTime(requestTime)
return builder
}
// WithRequiredObjectId adds the object identifier.
// May be prefilled by audit middleware (if the identifier can be extracted from the url path).
func (builder *AuditEventBuilder) WithRequiredObjectId(objectId string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequiredObjectId(objectId)
return builder
}
// WithRequiredObjectType adds the object type.
// May be prefilled by audit middleware (if the type can be extracted from the url path).
func (builder *AuditEventBuilder) WithRequiredObjectType(objectType SingularType) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequiredObjectType(objectType)
return builder
}
// WithRequiredOperation adds the name of the service method or operation.
//
// Format: stackit.<product>.<version>.<type-chain>.<operation>
// Where:
//
// Product: The name of the service in lowercase
// Version: Optional API version
// Type-Chain: Chained path to object
// Operation: The name of the operation in lowercase
//
// Examples:
//
// "stackit.resource-manager.v1.organizations.create"
// "stackit.authorization.v1.projects.volumes.create"
// "stackit.authorization.v2alpha.projects.volumes.create"
// "stackit.authorization.v2.folders.move"
// "stackit.resource-manager.health"
func (builder *AuditEventBuilder) WithRequiredOperation(operation string) *AuditEventBuilder {
builder.auditLogEntryBuilder.auditMetadata.AuditOperationName = operation
return builder
}
// WithAuditPermission adds the IAM permission
//
// Examples:
//
// "resourcemanager.project.edit"
func (builder *AuditEventBuilder) WithAuditPermission(permission string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithAuditPermission(permission)
return builder
}
// WithAuditPermissionCheckResult adds the IAM permission check result
func (builder *AuditEventBuilder) WithAuditPermissionCheckResult(permissionCheckResult bool) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithAuditPermissionCheckResult(permissionCheckResult)
return builder
}
// WithLabels adds A set of user-defined (key, value) data that provides additional
// information about the log entry.
func (builder *AuditEventBuilder) WithLabels(labels map[string]string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithLabels(labels)
return builder
}
// WithNumResponseItems adds the number of items returned to the client if applicable.
func (builder *AuditEventBuilder) WithNumResponseItems(numResponseItems int64) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithNumResponseItems(numResponseItems)
return builder
}
// WithEventType overwrites the default event type EventTypeAdminActivity
func (builder *AuditEventBuilder) WithEventType(eventType EventType) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithEventType(eventType)
return builder
}
// WithDetails adds an optional details object to the audit log entry
func (builder *AuditEventBuilder) WithDetails(details map[string]interface{}) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithDetails(details)
return builder
}
// WithSeverity overwrites the default log severity level auditV1.LogSeverity_LOG_SEVERITY_DEFAULT
func (builder *AuditEventBuilder) WithSeverity(severity auditV1.LogSeverity) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithSeverity(severity)
return builder
}
// WithStatusCode adds the (http) response status code
func (builder *AuditEventBuilder) WithStatusCode(statusCode int) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithStatusCode(statusCode)
return builder
}
// WithResponseBodyBytes adds the response body as bytes (json or protobuf expected)
func (builder *AuditEventBuilder) WithResponseBodyBytes(responseBody *[]byte) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithResponseBodyBytes(responseBody)
return builder
}
// WithResponseHeaders adds response headers
func (builder *AuditEventBuilder) WithResponseHeaders(responseHeaders map[string][]string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithResponseHeaders(responseHeaders)
return builder
}
// WithResponseTime adds the time when the response is sent
func (builder *AuditEventBuilder) WithResponseTime(responseTime time.Time) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithResponseTime(responseTime)
return builder
}
// WithVisibility overwrites the default visibility auditV1.Visibility_VISIBILITY_PUBLIC
func (builder *AuditEventBuilder) WithVisibility(visibility auditV1.Visibility) *AuditEventBuilder {
builder.visibility = visibility
return builder
}
// IsBuilt returns the status whether the cloud event has been built
func (builder *AuditEventBuilder) IsBuilt() bool {
return builder.built
}
// Build constructs the CloudEvent.
//
// Parameters:
// - A context object
// - A sequence number. AuditEventBuilder.NextSequenceNumber can be used to get the next SequenceNumber.
//
// Returns:
// - The CloudEvent containing the audit log entry
// - The RoutableIdentifier required for routing the cloud event
// - The operation name
// - Error if the event cannot be built
func (builder *AuditEventBuilder) Build(ctx context.Context, sequenceNumber SequenceNumber) (*CloudEvent, *RoutableIdentifier, string, error) {
if builder.auditLogEntryBuilder == nil {
return nil, nil, "", fmt.Errorf("audit log entry builder not set")
}
auditLogEntry, err := builder.auditLogEntryBuilder.Build(ctx, sequenceNumber)
if err != nil {
return nil, nil, "", err
}
objectId := builder.auditLogEntryBuilder.auditParams.ObjectId
objectType := builder.auditLogEntryBuilder.auditParams.ObjectType
ctx, span := builder.tracer.Start(ctx, "create-audit-event")
defer span.End()
w3cTraceParent := TraceParentFromSpan(span)
var traceParent = &w3cTraceParent
var traceState *string = nil
visibility := builder.visibility
operation := auditLogEntry.ProtoPayload.OperationName
routingIdentifier := NewAuditRoutingIdentifier(objectId, objectType)
// Validate and serialize the protobuf event into a cloud event
_, validateSerializeSpan := builder.tracer.Start(ctx, "validate-and-serialize-audit-event")
cloudEvent, err := (*builder.api).ValidateAndSerializeWithTrace(auditLogEntry, visibility, routingIdentifier, traceParent, traceState)
validateSerializeSpan.End()
if err != nil {
return nil, nil, "", err
}
builder.built = true
return cloudEvent,
routingIdentifier,
operation,
nil
}

616
audit/api/builder_test.go Normal file
View file

@ -0,0 +1,616 @@
package api
import (
"context"
"dev.azure.com/schwarzit/schwarzit.stackit-core-platform/audit-go.git/audit/utils"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/audit-go.git/gen/go/audit/v1"
"fmt"
"github.com/bufbuild/protovalidate-go"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/wrapperspb"
"testing"
"time"
)
func Test_getObjectIdAndTypeFromAuditParams(t *testing.T) {
t.Run(
"object id empty", func(t *testing.T) {
objectId, objectType, objectTypePlural, err := getObjectIdAndTypeFromAuditParams(context.Background(), &AuditParameters{})
assert.EqualError(t, err, "object id missing")
assert.Equal(t, "", objectId)
assert.Nil(t, objectType)
assert.Nil(t, objectTypePlural)
},
)
t.Run(
"object type empty", func(t *testing.T) {
objectId, objectType, objectTypePlural, err := getObjectIdAndTypeFromAuditParams(context.Background(), &AuditParameters{ObjectId: "value"})
assert.EqualError(t, err, "singular type missing")
assert.Equal(t, "", objectId)
assert.Nil(t, objectType)
assert.Nil(t, objectTypePlural)
},
)
t.Run(
"object id and invalid type set", func(t *testing.T) {
objectId, objectType, objectTypePlural, err := getObjectIdAndTypeFromAuditParams(
context.Background(), &AuditParameters{
ObjectId: "value",
ObjectType: AsSingularType("invalid"),
},
)
assert.EqualError(t, err, "unknown singular type")
assert.Equal(t, "", objectId)
assert.Nil(t, objectType)
assert.Nil(t, objectTypePlural)
},
)
t.Run(
"object id and type set", func(t *testing.T) {
objectId, objectType, objectTypePlural, err := getObjectIdAndTypeFromAuditParams(
context.Background(), &AuditParameters{
ObjectId: "value",
ObjectType: SingularTypeProject,
},
)
assert.NoError(t, err)
assert.Equal(t, "value", objectId)
assert.Equal(t, SingularTypeProject, *objectType)
assert.Equal(t, PluralTypeProject, *objectTypePlural)
},
)
}
func Test_AuditLogEntryBuilder(t *testing.T) {
t.Run("required only", func(t *testing.T) {
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")
logEntry, err := builder.Build(context.Background(), SequenceNumber(1))
assert.NoError(t, err)
assert.NotNil(t, logEntry)
assert.Equal(t, "projects/1/logs/admin-activity", 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, "Christian.Schaible@novatec-gmbh.de", authenticationInfo.PrincipalEmail)
assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", 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, "127.0.0.1", requestMetadata.CallerIp)
assert.Equal(t, "custom", requestMetadata.CallerSuppliedUserAgent)
requestAttributes := requestMetadata.RequestAttributes
assert.NotNil(t, requestAttributes)
assert.Equal(t, "/", requestAttributes.Path)
assert.NotNil(t, requestAttributes.Time)
assert.Equal(t, "localhost", requestAttributes.Host)
assert.Equal(t, auditV1.AttributeContext_HTTP_METHOD_POST, requestAttributes.Method)
assert.Nil(t, requestAttributes.Id)
assert.Equal(t, "https", requestAttributes.Scheme)
assert.Equal(t, map[string]string{"user-agent": "custom"}, requestAttributes.Headers)
assert.Nil(t, requestAttributes.Query)
assert.Equal(t, "HTTP/1.1", requestAttributes.Protocol)
requestAttributesAuth := requestAttributes.Auth
assert.NotNil(t, requestAttributesAuth)
assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63/https%3A%2F%2Faccounts.dev.stackit.cloud", requestAttributesAuth.Principal)
assert.Equal(t, []string{"stackit-portal-login-dev-client-id"}, requestAttributesAuth.Audiences)
assert.NotNil(t, requestAttributesAuth.Claims)
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 details", 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"}
responseBodyBytes, err := ResponseBodyToBytes(responseBody)
assert.NoError(t, err)
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).
WithResponseBodyBytes(responseBodyBytes).
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.Equal(t, "projects/1/logs/system-event", logEntry.LogName)
assert.Equal(t, map[string]string{"key": "label"}, logEntry.Labels)
assert.Nil(t, logEntry.TraceState)
assert.Nil(t, logEntry.TraceParent)
assert.Equal(t, auditV1.LogSeverity_LOG_SEVERITY_ERROR, logEntry.Severity)
assert.NotNil(t, logEntry.Timestamp)
assert.Equal(t, "correlationId", *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, "Christian.Schaible@novatec-gmbh.de", authenticationInfo.PrincipalEmail)
assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", authenticationInfo.PrincipalId)
assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
assert.Nil(t, authenticationInfo.ServiceAccountName)
assert.Equal(t, []*auditV1.AuthorizationInfo{{
Resource: "projects/1",
Permission: &permission,
Granted: &permissionCheckResult,
}}, logEntry.ProtoPayload.AuthorizationInfo)
expectedMetadata, _ := structpb.NewStruct(details)
assert.Equal(t, expectedMetadata, 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, "127.0.0.1", requestMetadata.CallerIp)
assert.Equal(t, "custom", requestMetadata.CallerSuppliedUserAgent)
requestAttributes := requestMetadata.RequestAttributes
assert.NotNil(t, requestAttributes)
assert.Equal(t, "/", requestAttributes.Path)
assert.NotNil(t, requestAttributes.Time)
assert.Equal(t, "localhost", requestAttributes.Host)
assert.Equal(t, auditV1.AttributeContext_HTTP_METHOD_POST, requestAttributes.Method)
assert.Equal(t, "requestId", *requestAttributes.Id)
assert.Equal(t, "https", requestAttributes.Scheme)
assert.Equal(t, map[string]string{"user-agent": "custom"}, requestAttributes.Headers)
assert.Nil(t, requestAttributes.Query)
assert.Equal(t, "HTTP/1.1", requestAttributes.Protocol)
requestAttributesAuth := requestAttributes.Auth
assert.NotNil(t, requestAttributesAuth)
assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63/https%3A%2F%2Faccounts.dev.stackit.cloud", requestAttributesAuth.Principal)
assert.Equal(t, []string{"stackit-portal-login-dev-client-id"}, requestAttributesAuth.Audiences)
assert.NotNil(t, requestAttributesAuth.Claims)
expectedResponse, _ := structpb.NewStruct(responseBody)
assert.Equal(t, "projects/1", logEntry.ProtoPayload.ResourceName)
assert.Equal(t, expectedResponse, logEntry.ProtoPayload.Response)
responseMetadata := logEntry.ProtoPayload.ResponseMetadata
assert.NotNil(t, responseMetadata)
assert.Nil(t, responseMetadata.ErrorDetails)
assert.Equal(t, "Client error", *responseMetadata.ErrorMessage)
assert.Equal(t, wrapperspb.Int32(400), responseMetadata.StatusCode)
responseAttributes := responseMetadata.ResponseAttributes
assert.NotNil(t, responseAttributes)
assert.Equal(t, map[string]string{"key": "header"}, responseAttributes.Headers)
assert.Equal(t, wrapperspb.Int64(10), responseAttributes.NumResponseItems)
assert.Equal(t, wrapperspb.Int64(18), 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)
})
}
func Test_AuditEventBuilder(t *testing.T) {
t.Run("required only", func(t *testing.T) {
api, _ := NewMockAuditApi()
sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator()
tracer := otel.Tracer("test")
objectId := uuid.NewString()
operation := "stackit.demo-service.v1.operation"
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")
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)
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_PUBLIC, 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/admin-activity", 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, "Christian.Schaible@novatec-gmbh.de", authenticationInfo.PrincipalEmail)
assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", 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, "127.0.0.1", requestMetadata.CallerIp)
assert.Equal(t, "custom", requestMetadata.CallerSuppliedUserAgent)
requestAttributes := requestMetadata.RequestAttributes
assert.NotNil(t, requestAttributes)
assert.Equal(t, "/", requestAttributes.Path)
assert.NotNil(t, requestAttributes.Time)
assert.Equal(t, "localhost", requestAttributes.Host)
assert.Equal(t, auditV1.AttributeContext_HTTP_METHOD_POST, requestAttributes.Method)
assert.Nil(t, requestAttributes.Id)
assert.Equal(t, "https", requestAttributes.Scheme)
assert.Equal(t, map[string]string{"user-agent": "custom"}, requestAttributes.Headers)
assert.Nil(t, requestAttributes.Query)
assert.Equal(t, "HTTP/1.1", requestAttributes.Protocol)
requestAttributesAuth := requestAttributes.Auth
assert.NotNil(t, requestAttributesAuth)
assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63/https%3A%2F%2Faccounts.dev.stackit.cloud", requestAttributesAuth.Principal)
assert.Equal(t, []string{"stackit-portal-login-dev-client-id"}, requestAttributesAuth.Audiences)
assert.NotNil(t, requestAttributesAuth.Claims)
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 details", 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"}
responseBodyBytes, err := ResponseBodyToBytes(responseBody)
assert.NoError(t, err)
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).
WithResponseBodyBytes(responseBodyBytes).
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)
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.Equal(t, map[string]string{"key": "label"}, logEntry.Labels)
assert.Nil(t, logEntry.TraceState)
assert.Nil(t, logEntry.TraceParent)
assert.Equal(t, auditV1.LogSeverity_LOG_SEVERITY_ERROR, logEntry.Severity)
assert.NotNil(t, logEntry.Timestamp)
assert.Equal(t, "correlationId", *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, "Christian.Schaible@novatec-gmbh.de", authenticationInfo.PrincipalEmail)
assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", authenticationInfo.PrincipalId)
assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
assert.Nil(t, authenticationInfo.ServiceAccountName)
assert.Equal(t, []*auditV1.AuthorizationInfo{{
Resource: fmt.Sprintf("projects/%s", objectId),
Permission: &permission,
Granted: &permissionCheckResult,
}}, logEntry.ProtoPayload.AuthorizationInfo)
expectedMetadata, _ := structpb.NewStruct(details)
assert.Equal(t, expectedMetadata, 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, "127.0.0.1", requestMetadata.CallerIp)
assert.Equal(t, "custom", requestMetadata.CallerSuppliedUserAgent)
requestAttributes := requestMetadata.RequestAttributes
assert.NotNil(t, requestAttributes)
assert.Equal(t, "/", requestAttributes.Path)
assert.NotNil(t, requestAttributes.Time)
assert.Equal(t, "localhost", requestAttributes.Host)
assert.Equal(t, auditV1.AttributeContext_HTTP_METHOD_POST, requestAttributes.Method)
assert.Equal(t, "requestId", *requestAttributes.Id)
assert.Equal(t, "https", requestAttributes.Scheme)
assert.Equal(t, map[string]string{"user-agent": "custom"}, requestAttributes.Headers)
assert.Nil(t, requestAttributes.Query)
assert.Equal(t, "HTTP/1.1", requestAttributes.Protocol)
requestAttributesAuth := requestAttributes.Auth
assert.NotNil(t, requestAttributesAuth)
assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63/https%3A%2F%2Faccounts.dev.stackit.cloud", requestAttributesAuth.Principal)
assert.Equal(t, []string{"stackit-portal-login-dev-client-id"}, requestAttributesAuth.Audiences)
assert.NotNil(t, requestAttributesAuth.Claims)
expectedResponse, _ := structpb.NewStruct(responseBody)
assert.Equal(t, fmt.Sprintf("projects/%s", objectId), logEntry.ProtoPayload.ResourceName)
assert.Equal(t, expectedResponse, logEntry.ProtoPayload.Response)
responseMetadata := logEntry.ProtoPayload.ResponseMetadata
assert.NotNil(t, responseMetadata)
assert.Nil(t, responseMetadata.ErrorDetails)
assert.Equal(t, "Client error", *responseMetadata.ErrorMessage)
assert.Equal(t, wrapperspb.Int32(400), responseMetadata.StatusCode)
responseAttributes := responseMetadata.ResponseAttributes
assert.NotNil(t, responseAttributes)
assert.Equal(t, map[string]string{"key": "header"}, responseAttributes.Headers)
assert.Equal(t, wrapperspb.Int64(10), responseAttributes.NumResponseItems)
assert.Equal(t, wrapperspb.Int64(18), 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("no entry builder", func(t *testing.T) {
api, _ := NewMockAuditApi()
sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator()
tracer := otel.Tracer("test")
cloudEvent, routingIdentifier, operation, err := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", "worker-id", "eu01").
WithAuditLogEntryBuilder(nil).Build(context.Background(), SequenceNumber(1))
assert.EqualError(t, err, "audit log entry builder not set")
assert.Nil(t, cloudEvent)
assert.Nil(t, routingIdentifier)
assert.Equal(t, "", operation)
})
t.Run("next sequence number", func(t *testing.T) {
api, _ := NewMockAuditApi()
sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator()
tracer := otel.Tracer("test")
builder := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", "worker-id", "eu01")
assert.Equal(t, SequenceNumber(0), builder.NextSequenceNumber())
assert.Equal(t, SequenceNumber(1), builder.NextSequenceNumber())
})
t.Run("revert sequence number", func(t *testing.T) {
api, _ := NewMockAuditApi()
sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator()
tracer := otel.Tracer("test")
builder := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", "worker-id", "eu01")
assert.Equal(t, SequenceNumber(0), builder.NextSequenceNumber())
builder.RevertSequenceNumber()
assert.Equal(t, SequenceNumber(0), builder.NextSequenceNumber())
})
}

View file

@ -9,6 +9,8 @@ import (
"fmt"
"github.com/google/uuid"
"go.opentelemetry.io/otel/trace"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"google.golang.org/protobuf/types/known/wrapperspb"
@ -904,3 +906,30 @@ func StringAttributeFromMetadata(metadata map[string][]string, name string) stri
}
return value
}
// ResponseBodyToBytes converts a JSON or Protobuf response into a byte array
func ResponseBodyToBytes(response any) (*[]byte, error) {
if response == nil {
return nil, nil
}
responseBytes, isBytes := response.([]byte)
if isBytes {
return &responseBytes, nil
}
responseProtoMessage, isProtoMessage := response.(proto.Message)
if isProtoMessage {
responseJson, err := protojson.Marshal(responseProtoMessage)
if err != nil {
return nil, err
}
return &responseJson, nil
} else {
responseJson, err := json.Marshal(response)
if err != nil {
return nil, err
}
return &responseJson, nil
}
}

View file

@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"google.golang.org/protobuf/types/known/wrapperspb"
@ -1030,3 +1031,64 @@ func Test_StringAttributeFromMetadata(t *testing.T) {
assert.Equal(t, "value2", attribute)
})
}
func Test_ResponseBodyToBytes(t *testing.T) {
t.Run(
"nil response body", func(t *testing.T) {
bytes, err := ResponseBodyToBytes(nil)
assert.Nil(t, bytes)
assert.Nil(t, err)
},
)
t.Run(
"bytes", func(t *testing.T) {
responseBody := []byte("data")
bytes, err := ResponseBodyToBytes(responseBody)
assert.Nil(t, err)
assert.Equal(t, &responseBody, bytes)
},
)
t.Run(
"Protobuf message", func(t *testing.T) {
protobufMessage := auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: string(SingularTypeProject)}
bytes, err := ResponseBodyToBytes(&protobufMessage)
assert.Nil(t, err)
expected, err := protojson.Marshal(&protobufMessage)
assert.Nil(t, err)
assert.Equal(t, &expected, bytes)
},
)
t.Run(
"struct", func(t *testing.T) {
type CustomObject struct {
Value string
}
responseBody := CustomObject{Value: "data"}
bytes, err := ResponseBodyToBytes(responseBody)
assert.Nil(t, err)
expected, err := json.Marshal(responseBody)
assert.Nil(t, err)
assert.Equal(t, &expected, bytes)
},
)
t.Run(
"map", func(t *testing.T) {
responseBody := map[string]interface{}{"value": "data"}
bytes, err := ResponseBodyToBytes(responseBody)
assert.Nil(t, err)
expected, err := json.Marshal(responseBody)
assert.Nil(t, err)
assert.Equal(t, &expected, bytes)
},
)
}