package api import ( "context" "errors" "fmt" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" "strings" "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" ) type ContextKey string const ContextKeyTopic ContextKey = "topic" var ErrNoTopicNameProvided = errors.New("no topic name provided") var ErrTopicNameEmpty = errors.New("empty topic name provided") // DynamicLegacyAuditApi is an implementation of AuditApi to send events to the legacy audit log system // by setting the topic name explicitly in the context with the key "topic". // // Note: The implementation will be deprecated and replaced with the "routableAuditApi" once the new audit log routing is implemented type DynamicLegacyAuditApi struct { messagingApi messaging.Api tracer trace.Tracer validator ProtobufValidator } // NewDynamicLegacyAuditApi can be used to initialize the audit log api. // // Note: The NewLegacyAuditApi method will be deprecated and replaced with "newRoutableAuditApi" once the new audit log routing is implemented func NewDynamicLegacyAuditApi( messagingApi messaging.Api, validator ProtobufValidator, ) (AuditApi, error) { if messagingApi == nil { return nil, ErrMessagingApiNil } // Audit api var auditApi AuditApi = &DynamicLegacyAuditApi{ messagingApi: messagingApi, tracer: otel.Tracer("dynamic-legacy-audit-api"), validator: validator, } return auditApi, nil } // Log implements AuditApi.Log func (a *DynamicLegacyAuditApi) Log( ctx context.Context, event *auditV1.AuditLogEntry, visibility auditV1.Visibility, routableIdentifier *RoutableIdentifier, ) error { cloudEvent, err := a.ValidateAndSerialize(ctx, event, visibility, routableIdentifier) if err != nil { return err } return a.Send(ctx, routableIdentifier, cloudEvent) } // ValidateAndSerialize implements AuditApi.ValidateAndSerialize. // It serializes the event into the byte representation of the legacy audit log system. func (a *DynamicLegacyAuditApi) ValidateAndSerialize( ctx context.Context, event *auditV1.AuditLogEntry, visibility auditV1.Visibility, routableIdentifier *RoutableIdentifier, ) (*CloudEvent, error) { ctx, span := a.tracer.Start(ctx, "validate-and-serialize") defer span.End() routableEvent, err := validateAndSerializePartially(a.validator, event, visibility, routableIdentifier) if err != nil { return nil, err } // Reject event type data-access as the downstream services // cannot handle it at the moment if strings.HasSuffix(event.LogName, string(EventTypeDataAccess)) { return nil, ErrUnsupportedEventTypeDataAccess } // Do nothing with the serialized data in the legacy solution _, err = proto.Marshal(routableEvent) if err != nil { return nil, err } // Convert attributes legacyBytes, err := convertAndSerializeIntoLegacyFormat(event, routableEvent) if err != nil { return nil, err } traceParent, traceState := TraceParentAndStateFromContext(ctx) message := CloudEvent{ SpecVersion: "1.0", Source: event.ProtoPayload.ServiceName, Id: event.InsertId, Time: event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(), DataContentType: ContentTypeCloudEventsJson, DataType: DataTypeLegacyAuditEventV1, Subject: event.ProtoPayload.ResourceName, Data: legacyBytes, TraceParent: &traceParent, TraceState: &traceState, } return &message, nil } // Send implements AuditApi.Send // // Requires to have the topic name set as key "topic" in the context. func (a *DynamicLegacyAuditApi) Send( ctx context.Context, routableIdentifier *RoutableIdentifier, cloudEvent *CloudEvent, ) error { rawTopicName := ctx.Value(ContextKeyTopic) if rawTopicName == nil { return ErrNoTopicNameProvided } topicName := fmt.Sprintf("%s", rawTopicName) if topicName == "" { return ErrTopicNameEmpty } if !TopicNamePattern.MatchString(topicName) { return fmt.Errorf("invalid topic name: %s - "+ "expected stackit-platform/t/swz/audit-log/{region}/{version}/{eventSource}/{additionalParts} "+ "where region is one of [conway, eu01, eu02, sx-stoi01], version is vX.Y, eventSource is the service name "+ "and additionalParts is a describing string the audit log relates to or 'events'", topicName) } var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: topicName} if cloudEvent != nil && cloudEvent.TraceParent != nil && cloudEvent.TraceState != nil { ctx = AddTraceParentAndStateToContext(ctx, *cloudEvent.TraceParent, *cloudEvent.TraceState) } ctx, span := a.tracer.Start(ctx, "send") defer span.End() return send(topicNameResolver, a.messagingApi, ctx, routableIdentifier, cloudEvent) }