package api import ( "context" "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/utils" auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1" "errors" "fmt" "github.com/google/uuid" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" "time" ) const quadZero = "0.0.0.0" 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 ObjectType ResponseBody any // Log severity Severity auditV1.LogSeverity } func getObjectIdAndTypeFromAuditParams( auditParams *AuditParameters, ) (string, *ObjectType, error) { objectId := auditParams.ObjectId if objectId == "" { return "", nil, errors.New("object id missing") } var objectType *ObjectType if auditParams.ObjectType != "" { objectType = &auditParams.ObjectType } if objectType == nil { return "", nil, errors.New("object type missing") } if err := objectType.IsSupportedType(); err != nil { return "", nil, err } return objectId, objectType, 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 // Opentelemetry tracer tracer trace.Tracer // 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: &ApiRequest{}, RequestClientIP: quadZero, 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: "", tracer: otel.Tracer("audit-log-entry-builder"), workerId: "", } } func (builder *AuditLogEntryBuilder) AsSystemEvent() *AuditLogEntryBuilder { if builder.auditRequest.Request == nil { builder.auditRequest.Request = &ApiRequest{} } if builder.auditRequest.Request.Header == nil { builder.auditRequest.Request.Header = map[string][]string{"user-agent": {"none"}} } if builder.auditRequest.Request.Host == "" { builder.auditRequest.Request.Host = quadZero } if builder.auditRequest.Request.Method == "" { builder.auditRequest.Request.Method = "OTHER" } if builder.auditRequest.Request.Scheme == "" { builder.auditRequest.Request.Scheme = "none" } if builder.auditRequest.Request.Proto == "" { builder.auditRequest.Request.Proto = "none" } if builder.auditRequest.Request.URL.Path == "" { builder.auditRequest.Request.URL.Path = "none" } if builder.auditRequest.RequestClientIP == "" { builder.auditRequest.RequestClientIP = quadZero } builder.WithEventType(EventTypeSystemEvent) return builder } // 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 ObjectType) *AuditLogEntryBuilder { builder.auditParams.ObjectType = objectType return builder } // WithRequiredOperation adds the name of the service method or operation. // // Format: stackit.... // 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 } // WithResponseBody adds the response body to the builder and transforms it in the Build method (json serializable or protobuf message expected) func (builder *AuditLogEntryBuilder) WithResponseBody(responseBody any) *AuditLogEntryBuilder { builder.auditParams.ResponseBody = responseBody return builder } // WithResponseBodyBytes adds the response body as bytes (serialized json or protobuf message expected) func (builder *AuditLogEntryBuilder) WithResponseBodyBytes(responseBody *[]byte) *AuditLogEntryBuilder { builder.auditResponse.ResponseBodyBytes = responseBody return builder } // 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) { _, span := builder.tracer.Start(ctx, "build-audit-log-entry") defer span.End() auditTime := time.Now() builder.auditMetadata.AuditTime = &auditTime objectId, objectType, err := getObjectIdAndTypeFromAuditParams(&builder.auditParams) if err != nil { return nil, err } if builder.auditResponse.ResponseBodyBytes != nil && builder.auditParams.ResponseBody != nil { return nil, errors.New("responseBodyBytes and responseBody set") } else if builder.auditParams.ResponseBody != nil { responseBytes, err := ResponseBodyToBytes(builder.auditParams.ResponseBody) if err != nil { return nil, err } builder.auditResponse.ResponseBodyBytes = responseBytes } resourceName := fmt.Sprintf("%s/%s", objectType.Plural(), objectId) var logIdentifier string var logType ObjectType if builder.auditParams.EventType == EventTypeSystemEvent { logIdentifier = SystemIdentifier.Identifier logType = ObjectTypeSystem } else { logIdentifier = objectId logType = *objectType } builder.auditMetadata.AuditInsertId = NewInsertId(time.Now().UTC(), builder.location, builder.workerId, uint64(sequenceNumber)) builder.auditMetadata.AuditLogName = fmt.Sprintf("%s/%s/logs/%s", logType.Plural(), logIdentifier, 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, ) } // 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 // Opentelemetry 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, // 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: otel.Tracer("audit-event-builder"), 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(number SequenceNumber) { (*builder.sequenceNumberGenerator).Revert(uint64(number)) } func (builder *AuditEventBuilder) AsSystemEvent() *AuditEventBuilder { builder.auditLogEntryBuilder.AsSystemEvent() builder.WithVisibility(auditV1.Visibility_VISIBILITY_PRIVATE) return builder } // WithAuditLogEntryBuilder overwrites the preconfigured AuditLogEntryBuilder func (builder *AuditEventBuilder) WithAuditLogEntryBuilder(auditLogEntryBuilder *AuditLogEntryBuilder) *AuditEventBuilder { builder.auditLogEntryBuilder = auditLogEntryBuilder 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 ObjectType) *AuditEventBuilder { builder.auditLogEntryBuilder.WithRequiredObjectType(objectType) return builder } // WithRequiredOperation adds the name of the service method or operation. // // Format: stackit.... // 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 } // WithResponseBody adds the response body to the builder and transforms it in the Build method (json serializable or protobuf message expected) func (builder *AuditEventBuilder) WithResponseBody(responseBody any) *AuditEventBuilder { builder.auditLogEntryBuilder.WithResponseBody(responseBody) return builder } // WithResponseBodyBytes adds the response body as bytes (serialized json or protobuf message expected) func (builder *AuditEventBuilder) WithResponseBodyBytes(responseBody *[]byte) *AuditEventBuilder { builder.auditLogEntryBuilder.WithResponseBodyBytes(responseBody) return builder } // 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, error) { if builder.auditLogEntryBuilder == nil { return nil, nil, fmt.Errorf("audit log entry builder not set") } ctx, span := builder.tracer.Start(ctx, "build-audit-event") defer span.End() visibility := builder.visibility objectId := builder.auditLogEntryBuilder.auditParams.ObjectId objectType := builder.auditLogEntryBuilder.auditParams.ObjectType var routingIdentifier *RoutableIdentifier if builder.auditLogEntryBuilder.auditParams.EventType == EventTypeSystemEvent { routingIdentifier = NewAuditRoutingIdentifier(uuid.Nil.String(), ObjectTypeSystem) if objectId == "" { objectId = uuid.Nil.String() builder.WithRequiredObjectId(objectId) } if objectType == "" { objectType = ObjectTypeSystem builder.WithRequiredObjectType(objectType) } } else { routingIdentifier = NewAuditRoutingIdentifier(objectId, objectType) } auditLogEntry, err := builder.auditLogEntryBuilder.Build(ctx, sequenceNumber) if err != nil { return nil, nil, err } // Validate and serialize the protobuf event into a cloud event cloudEvent, err := (*builder.api).ValidateAndSerialize(ctx, auditLogEntry, visibility, routingIdentifier) if err != nil { return nil, nil, err } builder.built = true return cloudEvent, routingIdentifier, nil }