mirror of
https://dev.azure.com/schwarzit/schwarzit.stackit-public/_git/audit-go
synced 2026-02-08 00:57:24 +00:00
Add event builder
This commit is contained in:
parent
ab40008fb1
commit
f8f0b48437
4 changed files with 1291 additions and 0 deletions
584
audit/api/builder.go
Normal file
584
audit/api/builder.go
Normal 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
616
audit/api/builder_test.go
Normal 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())
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue