package api import ( "context" "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/telemetry" "errors" "fmt" "strings" "github.com/google/uuid" "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/messaging" auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1" "google.golang.org/protobuf/proto" ) // ContentTypeCloudEventsProtobuf the cloudevents protobuf content-type sent in metadata of messages const ContentTypeCloudEventsProtobuf = "application/cloudevents+protobuf" const ContentTypeCloudEventsJson = "application/cloudevents+json; charset=UTF-8" // ErrAttributeIdentifierInvalid indicates that the object identifier // and the identifier in the checked attribute do not match var ErrAttributeIdentifierInvalid = errors.New("attribute identifier invalid") // ErrAttributeTypeInvalid indicates that an invalid type has been provided. var ErrAttributeTypeInvalid = errors.New("attribute type invalid") // ErrCloudEventNil states that the given cloud event is nil var ErrCloudEventNil = errors.New("cloud event nil") // ErrEventNil indicates that the event was nil var ErrEventNil = errors.New("event is nil") // ErrInvalidRoutableIdentifierForSystemEvent states that the routable identifier is not valid for a system event var ErrInvalidRoutableIdentifierForSystemEvent = errors.New("invalid identifier for system event") // ErrMessagingApiNil states that the messaging api is nil var ErrMessagingApiNil = errors.New("messaging api nil") // ErrObjectIdentifierNil indicates that the object identifier was nil var ErrObjectIdentifierNil = errors.New("object identifier is nil") // ErrObjectIdentifierVisibilityMismatch indicates that a reference mismatch was detected. // // Valid combinations are: // * Visibility: Public, ObjectIdentifier: // * Visibility: Private, ObjectIdentifier: var ErrObjectIdentifierVisibilityMismatch = errors.New("object reference visibility mismatch") // ErrTopicNameResolverNil states that the topic name resolve is nil var ErrTopicNameResolverNil = errors.New("topic name resolver nil") // ErrUnknownObjectType indicates that the given input is an unknown object type var ErrUnknownObjectType = errors.New("unknown object type") // ErrUnsupportedEventTypeDataAccess states that the event type "data-access" is currently not supported var ErrUnsupportedEventTypeDataAccess = errors.New("unsupported event type data access") // ErrUnsupportedObjectIdentifierType indicates that an unsupported object identifier type has been provided var ErrUnsupportedObjectIdentifierType = errors.New("unsupported object identifier type") // ErrUnsupportedRoutableType indicates that the given input is an unsupported routable type var ErrUnsupportedRoutableType = errors.New("unsupported routable type") func validateAndSerializePartially( validator *ProtobufValidator, event *auditV1.AuditLogEntry, visibility auditV1.Visibility, routableIdentifier *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 *ProtobufValidator, event *auditV1.AuditLogEntry, visibility auditV1.Visibility, routableIdentifier *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 *ProtobufValidator, event *auditV1.AuditLogEntry, visibility auditV1.Visibility, routableIdentifier *RoutableIdentifier, ) error { // Return error if the given event or object identifier is nil if event == nil { return ErrEventNil } if routableIdentifier == nil { return 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 ErrObjectIdentifierVisibilityMismatch } // Check that provided identifier type is supported if err := routableIdentifier.Type.IsSupportedType(); err != nil { if errors.Is(err, ErrUnknownObjectType) { return ErrUnsupportedRoutableType } return err } // Check identifier consistency across event attributes if strings.HasSuffix(event.LogName, string(EventTypeSystemEvent)) { if !(routableIdentifier.Identifier == SystemIdentifier.Identifier && routableIdentifier.Type == ObjectTypeSystem) { return 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 *TopicNameResolver, messagingApi *messaging.Api, ctx context.Context, routableIdentifier *RoutableIdentifier, cloudEvent *CloudEvent, ) error { // Check that given objects are not nil if topicNameResolver == nil { return ErrTopicNameResolverNil } if messagingApi == nil { return ErrMessagingApiNil } if cloudEvent == nil { return ErrCloudEventNil } if routableIdentifier == nil { return ErrObjectIdentifierNil } // Check that provided identifier type is supported if err := routableIdentifier.Type.IsSupportedType(); err != nil { if errors.Is(err, ErrUnknownObjectType) { return 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 := telemetry.AuditGoVersion if auditGoVersion != "" { applicationAttributes["cloudEvents:sdkversion"] = auditGoVersion } auditGoGrpcVersion := telemetry.AuditGoGrpcVersion if auditGoGrpcVersion != "" { applicationAttributes["cloudEvents:sdkgrpcversion"] = auditGoGrpcVersion } auditGoHttpVersion := telemetry.AuditGoHttpVersion if auditGoHttpVersion != "" { applicationAttributes["cloudEvents:sdkhttpversion"] = auditGoHttpVersion } return (*messagingApi).Send( ctx, topic, (*cloudEvent).Data, (*cloudEvent).DataContentType, applicationAttributes) } func isSystemIdentifier(identifier *RoutableIdentifier) bool { if identifier.Identifier == uuid.Nil.String() && identifier.Type == ObjectTypeSystem { return true } return false } func areIdentifiersIdentical(routableIdentifier *RoutableIdentifier, logName string) error { dataType, identifier := getTypeAndIdentifierFromString(logName) objectType := ObjectTypeFromPluralString(dataType) err := objectType.IsSupportedType() if err != nil { return err } return areTypeAndIdentifierIdentical(routableIdentifier, objectType, identifier) } func areTypeAndIdentifierIdentical(routableIdentifier *RoutableIdentifier, dataType ObjectType, identifier string) error { if routableIdentifier.Identifier != identifier { return ErrAttributeIdentifierInvalid } if routableIdentifier.Type != dataType { return ErrAttributeTypeInvalid } return nil } func getTypeAndIdentifierFromString(input string) (string, string) { parts := strings.Split(input, "/") dataType := parts[0] identifier := parts[1] return dataType, identifier }