package api import ( "context" "encoding/json" "errors" "fmt" "github.com/google/uuid" "net/url" "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" ) var ErrUnsupportedSeverity = errors.New("unsupported severity level") // 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(*RoutableIdentifier) (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.Api topicNameResolver *TopicNameResolver validator *ProtobufValidator } // NewLegacyAuditApi 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 NewLegacyAuditApi( messagingApi *messaging.Api, topicNameConfig LegacyTopicNameConfig, validator ProtobufValidator, ) (*AuditApi, error) { if messagingApi == nil { return nil, ErrMessagingApiNil } // Topic resolver if topicNameConfig.TopicName == "" { return nil, errors.New("topic name is required") } 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.AuditLogEntry, visibility auditV1.Visibility, routableIdentifier *RoutableIdentifier, ) error { return a.LogWithTrace(ctx, event, visibility, routableIdentifier, nil, nil) } // LogWithTrace implements AuditApi.LogWithTrace func (a *LegacyAuditApi) LogWithTrace( ctx context.Context, event *auditV1.AuditLogEntry, visibility auditV1.Visibility, routableIdentifier *RoutableIdentifier, traceParent *string, traceState *string, ) error { cloudEvent, err := a.ValidateAndSerializeWithTrace(event, visibility, routableIdentifier, traceParent, traceState) 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 *LegacyAuditApi) ValidateAndSerialize( event *auditV1.AuditLogEntry, visibility auditV1.Visibility, routableIdentifier *RoutableIdentifier, ) (*CloudEvent, error) { return a.ValidateAndSerializeWithTrace(event, visibility, routableIdentifier, nil, nil) } // ValidateAndSerializeWithTrace implements AuditApi.ValidateAndSerializeWithTrace. // It serializes the event into the byte representation of the legacy audit log system. func (a *LegacyAuditApi) ValidateAndSerializeWithTrace( event *auditV1.AuditLogEntry, visibility auditV1.Visibility, routableIdentifier *RoutableIdentifier, traceParent *string, traceState *string, ) (*CloudEvent, error) { routableEvent, err := validateAndSerializePartially(a.validator, event, visibility, routableIdentifier) 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.ProtoPayload.ServiceName, // TODO what is the correct id? id: uuid.NewString(), time: event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(), dataContentType: ContentTypeCloudEventsProtobuf, dataType: fmt.Sprintf("%v", routableEvent.ProtoReflect().Descriptor().FullName()), // TODO check if this is correct subject: event.ProtoPayload.ResourceName, data: legacyBytes, traceParent: traceParent, traceState: traceState, } return &message, nil } // Send implements AuditApi.Send func (a *LegacyAuditApi) Send( ctx context.Context, routableIdentifier *RoutableIdentifier, cloudEvent *CloudEvent, ) error { return send(a.topicNameResolver, a.messagingApi, ctx, routableIdentifier, cloudEvent) } // convertAndSerializeIntoLegacyFormat converts the protobuf events into the json serialized legacy audit log format func (a *LegacyAuditApi) convertAndSerializeIntoLegacyFormat( event *auditV1.AuditLogEntry, 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 { switch principalValue := principal.Authority.(type) { case *auditV1.ServiceAccountDelegationInfo_IdpPrincipal_: principals = append(principals, LegacyAuditEventPrincipal{ Id: principalValue.IdpPrincipal.PrincipalId, Email: &principalValue.IdpPrincipal.PrincipalEmail, }) case *auditV1.ServiceAccountDelegationInfo_SystemPrincipal_: principals = append(principals, LegacyAuditEventPrincipal{ Id: "system", }) default: return nil, errors.New("unsupported principal type") } } serviceAccountDelegationInfo = &LegacyAuditEventServiceAccountDelegationInfo{Principals: principals} } var request LegacyAuditEventRequest if event.ProtoPayload.RequestMetadata.RequestAttributes == nil { request = LegacyAuditEventRequest{ Endpoint: "none", } } else { var parameters map[string]interface{} = nil if event.ProtoPayload.RequestMetadata.RequestAttributes.Path != "" && event.ProtoPayload.RequestMetadata.RequestAttributes.Query != nil && *event.ProtoPayload.RequestMetadata.RequestAttributes.Query != "" { parameters = map[string]interface{}{} parsedUrl, err := url.Parse(fmt.Sprintf("%s?%s", event.ProtoPayload.RequestMetadata.RequestAttributes.Path, *event.ProtoPayload.RequestMetadata.RequestAttributes.Query)) if err != nil { return nil, err } for k, v := range parsedUrl.Query() { parameters[k] = v } } var body map[string]interface{} = nil if event.ProtoPayload.Request != nil { body = event.ProtoPayload.Request.AsMap() } var headers map[string]interface{} = nil if event.ProtoPayload.RequestMetadata.RequestAttributes.Headers != nil { headers = map[string]interface{}{} for key, value := range event.ProtoPayload.RequestMetadata.RequestAttributes.Headers { headers[key] = value } } request = LegacyAuditEventRequest{ Endpoint: event.ProtoPayload.RequestMetadata.RequestAttributes.Path, Parameters: ¶meters, Body: &body, Headers: &headers, } } if routableEvent.ObjectIdentifier == nil { return nil, ErrObjectIdentifierNil } // Context and event type var messageContext *LegacyAuditEventContext var eventType string switch routableEvent.ObjectIdentifier.Type { case string(SingularTypeProject): eventType = "ADMIN_ACTIVITY" messageContext = &LegacyAuditEventContext{ OrganizationId: nil, FolderId: nil, ProjectId: &routableEvent.ObjectIdentifier.Identifier, } case string(SingularTypeFolder): eventType = "ADMIN_ACTIVITY" messageContext = &LegacyAuditEventContext{ OrganizationId: nil, FolderId: &routableEvent.ObjectIdentifier.Identifier, ProjectId: nil, } case string(SingularTypeOrganization): eventType = "ADMIN_ACTIVITY" messageContext = &LegacyAuditEventContext{ OrganizationId: &routableEvent.ObjectIdentifier.Identifier, FolderId: nil, ProjectId: nil, } case string(SingularTypeSystem): eventType = "SYSTEM_EVENT" messageContext = nil default: return nil, ErrUnsupportedObjectIdentifierType } var visibility string switch routableEvent.Visibility { case auditV1.Visibility_VISIBILITY_PUBLIC: visibility = "PUBLIC" case auditV1.Visibility_VISIBILITY_PRIVATE: visibility = "PRIVATE" } // Details var details = event.ProtoPayload.Request.AsMap() // Result var result = event.ProtoPayload.Response.AsMap() // Severity var severity string switch event.Severity { case auditV1.LogSeverity_DEFAULT: fallthrough case auditV1.LogSeverity_DEBUG: fallthrough case auditV1.LogSeverity_INFO: fallthrough case auditV1.LogSeverity_NOTICE: fallthrough case auditV1.LogSeverity_WARNING: severity = "INFO" case auditV1.LogSeverity_ERROR: fallthrough case auditV1.LogSeverity_CRITICAL: fallthrough case auditV1.LogSeverity_ALERT: fallthrough case auditV1.LogSeverity_EMERGENCY: severity = "ERROR" default: return nil, ErrUnsupportedSeverity } // Instantiate the legacy event - missing values are filled with defaults legacyAuditEvent := LegacyAuditEvent{ Severity: severity, Visibility: visibility, EventType: eventType, EventTimeStamp: event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(), EventName: event.ProtoPayload.MethodName, SourceIpAddress: sourceIpAddress, UserAgent: userAgent, Initiator: LegacyAuditEventPrincipal{ Id: event.ProtoPayload.AuthenticationInfo.PrincipalId, Email: &event.ProtoPayload.AuthenticationInfo.PrincipalEmail, }, ServiceAccountDelegationInfo: serviceAccountDelegationInfo, Request: request, Context: messageContext, // TODO clarify ResourceId: &event.LogName, ResourceName: &event.ProtoPayload.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"` }