package api import ( "context" "errors" "fmt" "regexp" "strings" "github.com/google/uuid" "google.golang.org/protobuf/proto" auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1" internalTelemetry "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/internal/telemetry" pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common" pkgMessagingApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/api" ) var TopicNamePattern = regexp.MustCompile(`^topic://stackit-platform/t/swz/audit-log/(?:conway|eu01|eu02|sx-stoi01)/[Vv][1-9](?:\.\d)?/[A-Za-z0-9-]+/[A-Za-z0-9-/]+`) func ValidateAndSerializePartially( validator pkgAuditCommon.ProtobufValidator, event *auditV1.AuditLogEntry, visibility auditV1.Visibility, routableIdentifier *pkgAuditCommon.RoutableIdentifier, ) (*auditV1.RoutableAuditEvent, error) { // Check preconditions err := validateAuditLogEntry(validator, event, visibility, routableIdentifier) if err != nil { return nil, err } // Serialize the AuditLogEntry and wrap it into a RoutableAuditEvent routableEvent, err := newValidatedRoutableAuditEvent(validator, event, visibility, routableIdentifier) if err != nil { return nil, err } return routableEvent, nil } func newValidatedRoutableAuditEvent( validator pkgAuditCommon.ProtobufValidator, event *auditV1.AuditLogEntry, visibility auditV1.Visibility, routableIdentifier *pkgAuditCommon.RoutableIdentifier) (*auditV1.RoutableAuditEvent, error) { // Test serialization even if the data is dropped later when logging to the legacy solution auditEventBytes, err := proto.Marshal(event) if err != nil { return nil, err } payload := auditV1.UnencryptedData{ Data: auditEventBytes, ProtobufType: fmt.Sprintf("%v", event.ProtoReflect().Descriptor().FullName()), } routableEvent := auditV1.RoutableAuditEvent{ OperationName: event.ProtoPayload.OperationName, ObjectIdentifier: routableIdentifier.ToObjectIdentifier(), Visibility: visibility, Data: &auditV1.RoutableAuditEvent_UnencryptedData{UnencryptedData: &payload}, } err = validator.Validate(&routableEvent) if err != nil { return nil, err } return &routableEvent, nil } func validateAuditLogEntry( validator pkgAuditCommon.ProtobufValidator, event *auditV1.AuditLogEntry, visibility auditV1.Visibility, routableIdentifier *pkgAuditCommon.RoutableIdentifier, ) error { // Return error if the given event or object identifier is nil if event == nil { return pkgAuditCommon.ErrEventNil } if routableIdentifier == nil { return pkgAuditCommon.ErrObjectIdentifierNil } // Validate the actual event err := validator.Validate(event) if err != nil { return err } // Ensure that a valid object identifier is set if the event is public if isSystemIdentifier(routableIdentifier) && visibility == auditV1.Visibility_VISIBILITY_PUBLIC { return pkgAuditCommon.ErrObjectIdentifierVisibilityMismatch } // Check that provided identifier type is supported if err := routableIdentifier.Type.IsSupportedType(); err != nil { if errors.Is(err, pkgAuditCommon.ErrUnknownObjectType) { return pkgAuditCommon.ErrUnsupportedRoutableType } return err } // Check identifier consistency across event attributes if strings.HasSuffix(event.LogName, string(pkgAuditCommon.EventTypeSystemEvent)) { if routableIdentifier.Identifier != pkgAuditCommon.SystemIdentifier.Identifier || routableIdentifier.Type != pkgAuditCommon.ObjectTypeSystem { return pkgAuditCommon.ErrInvalidRoutableIdentifierForSystemEvent } // The resource name can either contain the system identifier or another resource identifier } else { if err := areIdentifiersIdentical(routableIdentifier, event.LogName); err != nil { return err } if err := areIdentifiersIdentical(routableIdentifier, event.ProtoPayload.ResourceName); err != nil { return err } } return nil } // Send implements AuditApi.Send func Send( topicNameResolver pkgAuditCommon.TopicNameResolver, messagingApi pkgMessagingApi.Api, ctx context.Context, routableIdentifier *pkgAuditCommon.RoutableIdentifier, cloudEvent *pkgAuditCommon.CloudEvent, ) error { // Check that given objects are not nil if topicNameResolver == nil { return pkgAuditCommon.ErrTopicNameResolverNil } if messagingApi == nil { return pkgAuditCommon.ErrMessagingApiNil } if cloudEvent == nil { return pkgAuditCommon.ErrCloudEventNil } if routableIdentifier == nil { return pkgAuditCommon.ErrObjectIdentifierNil } // Check that provided identifier type is supported if err := routableIdentifier.Type.IsSupportedType(); err != nil { if errors.Is(err, pkgAuditCommon.ErrUnknownObjectType) { return pkgAuditCommon.ErrUnsupportedRoutableType } return err } topic, err := topicNameResolver.Resolve(routableIdentifier) if err != nil { return err } // Naming according to AMQP protocol binding spec // https://github.com/cloudevents/spec/blob/main/cloudevents/bindings/amqp-protocol-binding.md applicationAttributes := make(map[string]any) applicationAttributes["cloudEvents:specversion"] = cloudEvent.SpecVersion applicationAttributes["cloudEvents:source"] = cloudEvent.Source applicationAttributes["cloudEvents:id"] = cloudEvent.Id applicationAttributes["cloudEvents:time"] = cloudEvent.Time.UnixMilli() applicationAttributes["cloudEvents:datacontenttype"] = cloudEvent.DataContentType applicationAttributes["cloudEvents:type"] = cloudEvent.DataType applicationAttributes["cloudEvents:subject"] = cloudEvent.Subject if cloudEvent.TraceParent != nil { applicationAttributes["cloudEvents:traceparent"] = *cloudEvent.TraceParent } if cloudEvent.TraceState != nil { applicationAttributes["cloudEvents:tracestate"] = *cloudEvent.TraceState } // Telemetry applicationAttributes["cloudEvents:sdklanguage"] = "go" auditGoVersion := internalTelemetry.AuditGoVersion if auditGoVersion != "" { applicationAttributes["cloudEvents:sdkversion"] = auditGoVersion } auditGoGrpcVersion := internalTelemetry.AuditGoGrpcVersion if auditGoGrpcVersion != "" { applicationAttributes["cloudEvents:sdkgrpcversion"] = auditGoGrpcVersion } auditGoHttpVersion := internalTelemetry.AuditGoHttpVersion if auditGoHttpVersion != "" { applicationAttributes["cloudEvents:sdkhttpversion"] = auditGoHttpVersion } return messagingApi.Send( ctx, topic, cloudEvent.Data, cloudEvent.DataContentType, applicationAttributes) } func isSystemIdentifier(identifier *pkgAuditCommon.RoutableIdentifier) bool { if identifier.Identifier == uuid.Nil.String() && identifier.Type == pkgAuditCommon.ObjectTypeSystem { return true } return false } func areIdentifiersIdentical(routableIdentifier *pkgAuditCommon.RoutableIdentifier, logName string) error { dataType, identifier := getTypeAndIdentifierFromString(logName) objectType := pkgAuditCommon.ObjectTypeFromPluralString(dataType) err := objectType.IsSupportedType() if err != nil { return err } return areTypeAndIdentifierIdentical(routableIdentifier, objectType, identifier) } func areTypeAndIdentifierIdentical(routableIdentifier *pkgAuditCommon.RoutableIdentifier, dataType pkgAuditCommon.ObjectType, identifier string) error { if routableIdentifier.Identifier != identifier { return pkgAuditCommon.ErrAttributeIdentifierInvalid } if routableIdentifier.Type != dataType { return pkgAuditCommon.ErrAttributeTypeInvalid } return nil } func getTypeAndIdentifierFromString(input string) (string, string) { parts := strings.Split(input, "/") dataType := parts[0] identifier := parts[1] return dataType, identifier }