package api import ( "context" "encoding/json" "errors" "fmt" "github.com/google/uuid" "time" "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/common-audit.git/audit/messaging" auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/common-audit.git/gen/go/audit/v1" "google.golang.org/protobuf/proto" ) // LegacyTopicNameResolver implements TopicNameResolver. // A hard-coded topic name is used, routing identifiers are ignored. type LegacyTopicNameResolver struct { topicName string } // Resolve implements TopicNameResolver.Resolve func (r *LegacyTopicNameResolver) Resolve(*RoutingIdentifier) (string, error) { return r.topicName, nil } // LegacyTopicNameConfig provides topic name information required for the topic name resolution. type LegacyTopicNameConfig struct { TopicName string } // LegacyAuditApi is an implementation of AuditApi to send events to the legacy audit log system. // // Note: The implementation will be deprecated and replaced with the "routableAuditApi" once the new audit log routing is implemented type LegacyAuditApi struct { messagingApi *messaging.MessagingApi topicNameResolver *TopicNameResolver validator *ProtobufValidator } // NewLegacyAuditApi can be used to initialize the audit log api with LegacyAuditLogConnectionDetails. // // Note: The NewLegacyAuditApi method will be deprecated and replaced with "newRoutableAuditApi" once the new audit log routing is implemented func NewLegacyAuditApi( messagingApi *messaging.MessagingApi, topicNameConfig LegacyTopicNameConfig, validator ProtobufValidator, ) (*AuditApi, error) { if messagingApi == nil { return nil, errors.New("messaging api nil") } // Topic resolver var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: topicNameConfig.TopicName} // Audit api var auditApi AuditApi = &LegacyAuditApi{ messagingApi: messagingApi, topicNameResolver: &topicNameResolver, validator: &validator, } return &auditApi, nil } // Log implements AuditApi.Log func (a *LegacyAuditApi) Log( ctx context.Context, event *auditV1.AuditEvent, visibility auditV1.Visibility, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier, ) error { cloudEvent, err := a.ValidateAndSerialize(event, visibility, routingIdentifier, objectIdentifier) if err != nil { return err } return a.Send(ctx, routingIdentifier, cloudEvent) } // ValidateAndSerialize implements AuditApi.ValidateAndSerialize. // It serializes the event into the byte representation of the legacy audit log system. func (a *LegacyAuditApi) ValidateAndSerialize( event *auditV1.AuditEvent, visibility auditV1.Visibility, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier, ) (*CloudEvent, error) { routableEvent, err := validateAndSerializePartially(a.validator, event, visibility, routingIdentifier, objectIdentifier) if err != nil { return nil, err } // Do nothing with the serialized data in the legacy solution _, err = proto.Marshal(routableEvent) if err != nil { return nil, err } // Convert attributes legacyBytes, err := a.convertAndSerializeIntoLegacyFormat(event, routableEvent) if err != nil { return nil, err } message := CloudEvent{ specVersion: "1.0", source: event.EventSource, id: uuid.NewString(), time: event.EventTimeStamp.AsTime(), dataContentType: "application/cloudevents+protobuf", dataType: fmt.Sprintf("%v", routableEvent.ProtoReflect().Descriptor().FullName()), data: legacyBytes, } return &message, nil } // Send implements AuditApi.Send func (a *LegacyAuditApi) Send( ctx context.Context, routingIdentifier *RoutingIdentifier, cloudEvent *CloudEvent, ) error { return send(a.topicNameResolver, a.messagingApi, ctx, routingIdentifier, cloudEvent) } // convertAndSerializeIntoLegacyFormat converts the protobuf events into the json serialized legacy audit log format func (a *LegacyAuditApi) convertAndSerializeIntoLegacyFormat( event *auditV1.AuditEvent, routableEvent *auditV1.RoutableAuditEvent, ) ([]byte, error) { // Source IP & User agent var sourceIpAddress string var userAgent string if event.ProtoPayload == nil || event.ProtoPayload.RequestMetadata == nil { sourceIpAddress = "0.0.0.0" userAgent = "none" } else { sourceIpAddress = event.ProtoPayload.RequestMetadata.CallerIp userAgent = event.ProtoPayload.RequestMetadata.CallerSuppliedUserAgent } // Principals var serviceAccountDelegationInfo *LegacyAuditEventServiceAccountDelegationInfo = nil if len(event.ProtoPayload.AuthenticationInfo.ServiceAccountDelegationInfo) > 0 { var principals []LegacyAuditEventPrincipal for _, principal := range event.ProtoPayload.AuthenticationInfo.ServiceAccountDelegationInfo { if principal != nil { p := LegacyAuditEventPrincipal{ Id: principal.GetFirstPartyPrincipal().Id, Email: &principal.GetFirstPartyPrincipal().PrincipalEmail, } principals = append(principals, p) } } serviceAccountDelegationInfo = &LegacyAuditEventServiceAccountDelegationInfo{Principals: principals} } // Request var request LegacyAuditEventRequest if event.Request == nil { request = LegacyAuditEventRequest{ Endpoint: "none", } } else { var parameters map[string]interface{} = nil if event.Request.Parameters != nil { parameters = event.Request.Parameters.AsMap() } var body map[string]interface{} = nil if event.Request.Body != nil { body = event.Request.Body.AsMap() } var headers map[string]interface{} = nil if event.Request.Headers != nil { headers = map[string]interface{}{} for _, header := range event.Request.Headers { if header != nil { headers[header.Key] = header.Value } } } request = LegacyAuditEventRequest{ Endpoint: event.Request.Endpoint, Parameters: ¶meters, Body: &body, Headers: &headers, } } // Context and event type var messageContext *LegacyAuditEventContext var eventType string switch ref := routableEvent.GetResourceReference().(type) { case *auditV1.RoutableAuditEvent_ObjectIdentifier: eventType = "ADMIN_ACTIVITY" if ref.ObjectIdentifier.Type == auditV1.ObjectType_OBJECT_TYPE_ORGANIZATION { messageContext = &LegacyAuditEventContext{ OrganizationId: nil, FolderId: nil, ProjectId: nil, } } else if ref.ObjectIdentifier.Type == auditV1.ObjectType_OBJECT_TYPE_FOLDER { messageContext = &LegacyAuditEventContext{ OrganizationId: nil, FolderId: nil, ProjectId: nil, } } else if ref.ObjectIdentifier.Type == auditV1.ObjectType_OBJECT_TYPE_PROJECT { messageContext = &LegacyAuditEventContext{ OrganizationId: nil, FolderId: nil, ProjectId: nil, } } else { return nil, ErrUnsupportedObjectIdentifierType } case *auditV1.RoutableAuditEvent_ObjectName: eventType = "SYSTEM_EVENT" messageContext = nil default: return nil, ErrUnsupportedResourceReferenceType } // Details var details map[string]interface{} = nil if event.Details != nil { details = event.Details.AsMap() } // Result var result map[string]interface{} = nil if event.Result != nil { result = event.Result.AsMap() } // Instantiate the legacy event - missing values are filled with defaults legacyAuditEvent := LegacyAuditEvent{ Severity: "INFO", Visibility: routableEvent.Visibility.String(), EventType: eventType, EventTimeStamp: event.EventTimeStamp.AsTime(), EventName: event.EventName, SourceIpAddress: sourceIpAddress, UserAgent: userAgent, Initiator: LegacyAuditEventPrincipal{ Id: event.Initiator.Id, Email: event.Initiator.Email, }, ServiceAccountDelegationInfo: serviceAccountDelegationInfo, Request: request, Context: messageContext, ResourceId: event.ResourceId, ResourceName: event.ResourceName, CorrelationId: event.CorrelationId, Result: &result, Details: &details, } bytes, err := json.Marshal(legacyAuditEvent) if err != nil { return nil, err } return bytes, nil } // LegacyAuditEvent has the format as follows: /* { "severity": "INFO", "visibility": "PUBLIC", "eventType": "ADMIN_ACTIVITY", "eventTimeStamp": "2019-08-24T14:15:22Z", "eventName": "Create organization", "sourceIpAddress": "127.0.0.1", "userAgent": "CLI", "initiator": { "id": "string", "email": "user@example.com" }, "serviceAccountDelegationInfo": { "principals": [ { "id": "string", "email": "user@example.com" } ] }, "request": { "endpoint": "string", "parameters": {}, "body": {}, "headers": { "Content-Type": "application/json" } }, "context": { "organizationId": "string", "folderId": "string", "projectId": "string" }, "resourceId": "string", "resourceName": "string", "correlationId": "string", "result": {}, "details": {} } */ type LegacyAuditEvent struct { Severity string `json:"severity"` Visibility string `json:"visibility"` EventType string `json:"eventType"` EventTimeStamp time.Time `json:"eventTimeStamp"` EventName string `json:"eventName"` SourceIpAddress string `json:"sourceIpAddress"` UserAgent string `json:"userAgent"` Initiator LegacyAuditEventPrincipal `json:"initiator"` ServiceAccountDelegationInfo *LegacyAuditEventServiceAccountDelegationInfo `json:"serviceAccountDelegationInfo"` Request LegacyAuditEventRequest `json:"request"` Context *LegacyAuditEventContext `json:"context"` ResourceId *string `json:"resourceId"` ResourceName *string `json:"resourceName"` CorrelationId *string `json:"correlationId"` Result *map[string]interface{} `json:"result"` Details *map[string]interface{} `json:"details"` } // LegacyAuditEventPrincipal is a representation for a principal's id (+optional email) information. type LegacyAuditEventPrincipal struct { Id string `json:"id"` Email *string `json:"email"` } // LegacyAuditEventServiceAccountDelegationInfo contains information about service account delegation. type LegacyAuditEventServiceAccountDelegationInfo struct { Principals []LegacyAuditEventPrincipal `json:"principals"` } // LegacyAuditEventRequest contains request information, which mirrors the action of the user and // the resulting changes within the system. type LegacyAuditEventRequest struct { Endpoint string `json:"endpoint"` Parameters *map[string]interface{} `json:"parameters"` Body *map[string]interface{} `json:"body"` Headers *map[string]interface{} `json:"headers"` } // LegacyAuditEventContext contains optional context information. type LegacyAuditEventContext struct { OrganizationId *string `json:"organizationId"` FolderId *string `json:"folderId"` ProjectId *string `json:"projectId"` }