diff --git a/audit/api/api.go b/audit/api/api.go index cbe3ceb..3c44236 100644 --- a/audit/api/api.go +++ b/audit/api/api.go @@ -201,7 +201,7 @@ type CloudEvent struct { type TopicNameResolver interface { // Resolve returns a topic name for the given object identifier - Resolve(objectIdentifier *RoutableIdentifier) (string, error) + Resolve(routableIdentifier *RoutableIdentifier) (string, error) } type RoutableIdentifier struct { diff --git a/audit/api/api_legacy.go b/audit/api/api_legacy.go index 178ffc8..0340de7 100644 --- a/audit/api/api_legacy.go +++ b/audit/api/api_legacy.go @@ -2,20 +2,14 @@ package api import ( "context" - "encoding/json" - "errors" - "fmt" - "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" + "errors" + "fmt" "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 { @@ -131,7 +125,7 @@ func (a *LegacyAuditApi) ValidateAndSerializeWithTrace( } // Convert attributes - legacyBytes, err := a.convertAndSerializeIntoLegacyFormat(event, routableEvent) + legacyBytes, err := convertAndSerializeIntoLegacyFormat(event, routableEvent) if err != nil { return nil, err } @@ -160,279 +154,3 @@ func (a *LegacyAuditApi) Send( 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_LOG_SEVERITY_DEFAULT: - fallthrough - case auditV1.LogSeverity_LOG_SEVERITY_DEBUG: - fallthrough - case auditV1.LogSeverity_LOG_SEVERITY_INFO: - fallthrough - case auditV1.LogSeverity_LOG_SEVERITY_NOTICE: - fallthrough - case auditV1.LogSeverity_LOG_SEVERITY_WARNING: - severity = "INFO" - case auditV1.LogSeverity_LOG_SEVERITY_ERROR: - fallthrough - case auditV1.LogSeverity_LOG_SEVERITY_CRITICAL: - fallthrough - case auditV1.LogSeverity_LOG_SEVERITY_ALERT: - fallthrough - case auditV1.LogSeverity_LOG_SEVERITY_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.OperationName, - SourceIpAddress: sourceIpAddress, - UserAgent: userAgent, - Initiator: LegacyAuditEventPrincipal{ - Id: event.ProtoPayload.AuthenticationInfo.PrincipalId, - Email: &event.ProtoPayload.AuthenticationInfo.PrincipalEmail, - }, - ServiceAccountDelegationInfo: serviceAccountDelegationInfo, - Request: request, - Context: messageContext, - 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"` -} diff --git a/audit/api/api_legacy_converter.go b/audit/api/api_legacy_converter.go new file mode 100644 index 0000000..6ce5fd1 --- /dev/null +++ b/audit/api/api_legacy_converter.go @@ -0,0 +1,288 @@ +package api + +import ( + auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/common-audit.git/gen/go/audit/v1" + "encoding/json" + "errors" + "fmt" + "net/url" + "time" +) + +var ErrUnsupportedSeverity = errors.New("unsupported severity level") + +// convertAndSerializeIntoLegacyFormat converts the protobuf events into the json serialized legacy audit log format +func 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_LOG_SEVERITY_DEFAULT: + fallthrough + case auditV1.LogSeverity_LOG_SEVERITY_DEBUG: + fallthrough + case auditV1.LogSeverity_LOG_SEVERITY_INFO: + fallthrough + case auditV1.LogSeverity_LOG_SEVERITY_NOTICE: + fallthrough + case auditV1.LogSeverity_LOG_SEVERITY_WARNING: + severity = "INFO" + case auditV1.LogSeverity_LOG_SEVERITY_ERROR: + fallthrough + case auditV1.LogSeverity_LOG_SEVERITY_CRITICAL: + fallthrough + case auditV1.LogSeverity_LOG_SEVERITY_ALERT: + fallthrough + case auditV1.LogSeverity_LOG_SEVERITY_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.OperationName, + SourceIpAddress: sourceIpAddress, + UserAgent: userAgent, + Initiator: LegacyAuditEventPrincipal{ + Id: event.ProtoPayload.AuthenticationInfo.PrincipalId, + Email: &event.ProtoPayload.AuthenticationInfo.PrincipalEmail, + }, + ServiceAccountDelegationInfo: serviceAccountDelegationInfo, + Request: request, + Context: messageContext, + 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"` +} diff --git a/audit/api/api_legacy_converter_test.go b/audit/api/api_legacy_converter_test.go new file mode 100644 index 0000000..9284e15 --- /dev/null +++ b/audit/api/api_legacy_converter_test.go @@ -0,0 +1,20 @@ +package api + +import ( + auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/common-audit.git/gen/go/audit/v1" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_ConvertAndSerializeIntoLegacyFormat_NoObjectIdentifier(t *testing.T) { + event, _ := NewProjectAuditEvent(nil) + routableEvent := auditV1.RoutableAuditEvent{ + OperationName: event.ProtoPayload.OperationName, + Visibility: auditV1.Visibility_VISIBILITY_PUBLIC, + ObjectIdentifier: nil, + Data: nil, + } + + _, err := convertAndSerializeIntoLegacyFormat(event, &routableEvent) + assert.ErrorIs(t, err, ErrObjectIdentifierNil) +} diff --git a/audit/api/api_legacy_dynamic.go b/audit/api/api_legacy_dynamic.go new file mode 100644 index 0000000..342d31e --- /dev/null +++ b/audit/api/api_legacy_dynamic.go @@ -0,0 +1,152 @@ +package api + +import ( + "context" + "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" + "errors" + "fmt" + + "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 + 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, + 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 { + + return a.LogWithTrace(ctx, event, visibility, routableIdentifier, nil, nil) +} + +// LogWithTrace implements AuditApi.LogWithTrace +func (a *DynamicLegacyAuditApi) 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 *DynamicLegacyAuditApi) 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 *DynamicLegacyAuditApi) 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 := convertAndSerializeIntoLegacyFormat(event, routableEvent) + if err != nil { + return nil, err + } + + message := CloudEvent{ + SpecVersion: "1.0", + Source: event.ProtoPayload.ServiceName, + Id: event.InsertId, + Time: event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(), + DataContentType: ContentTypeCloudEventsProtobuf, + DataType: fmt.Sprintf("%v", routableEvent.ProtoReflect().Descriptor().FullName()), + 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 len(topicName) == 0 { + return ErrTopicNameEmpty + } + + var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: topicName} + + return send(&topicNameResolver, a.messagingApi, ctx, routableIdentifier, cloudEvent) +} diff --git a/audit/api/api_legacy_dynamic_test.go b/audit/api/api_legacy_dynamic_test.go new file mode 100644 index 0000000..253a565 --- /dev/null +++ b/audit/api/api_legacy_dynamic_test.go @@ -0,0 +1,432 @@ +package api + +import ( + "context" + "encoding/json" + "errors" + "log/slog" + "os" + "testing" + "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" + + "github.com/bufbuild/protovalidate-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestDynamicLegacyAuditApi(t *testing.T) { + slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil))) + + // Specify test timeout + ctx, cancelFn := context.WithTimeout(context.Background(), 120*time.Second) + defer cancelFn() + + // Start solace docker container + solaceContainer, err := messaging.NewSolaceContainer(context.Background()) + assert.NoError(t, err) + defer solaceContainer.Stop() + + // Instantiate the messaging api + messagingApi, err := messaging.NewAmqpApi(messaging.AmqpConfig{URL: solaceContainer.AmqpConnectionString}) + assert.NoError(t, err) + + // Validator + validator, err := protovalidate.New() + assert.NoError(t, err) + + topicSubscriptionTopicPattern := "audit-log/>" + traceParent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + traceState := "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE" + + // Check logging of organization events + t.Run("Log public organization event", func(t *testing.T) { + defer solaceContainer.StopOnError() + + // Create the queue and topic subscription in solace + queueName := "org-event-public-legacy" + assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) + assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern)) + + topicName := "topic://audit-log/eu01/v1/resource-manager/organization-created" + assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName)) + + // Instantiate audit api + auditApi, err := NewDynamicLegacyAuditApi( + messagingApi, + validator, + ) + assert.NoError(t, err) + + // Instantiate test data + event, objectIdentifier := NewOrganizationAuditEvent(nil) + + // Log the event to solace + visibility := auditV1.Visibility_VISIBILITY_PUBLIC + ctx := context.WithValue(ctx, ContextKeyTopic, topicName) + assert.NoError(t, (*auditApi).LogWithTrace( + ctx, + event, + visibility, + NewRoutableIdentifier(objectIdentifier), + &traceParent, + &traceState, + )) + + message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) + assert.NoError(t, err) + + validateSentMessage(t, topicName, message, event, &traceParent, &traceState) + }) + + t.Run("Log private organization event", func(t *testing.T) { + defer solaceContainer.StopOnError() + + // Create the queue and topic subscription in solace + queueName := "org-event-private-legacy" + assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) + assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern)) + + topicName := "topic://audit-log/eu01/v1/resource-manager/organization-created" + assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName)) + + // Instantiate audit api + auditApi, err := NewDynamicLegacyAuditApi( + messagingApi, + validator, + ) + assert.NoError(t, err) + + // Instantiate test data + event, objectIdentifier := NewOrganizationAuditEvent(nil) + + // Log the event to solace + visibility := auditV1.Visibility_VISIBILITY_PRIVATE + ctx := context.WithValue(ctx, ContextKeyTopic, topicName) + assert.NoError(t, (*auditApi).LogWithTrace( + ctx, + event, + visibility, + NewRoutableIdentifier(objectIdentifier), + &traceParent, + &traceState, + )) + + message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) + assert.NoError(t, err) + + validateSentMessage(t, topicName, message, event, &traceParent, &traceState) + }) + + // Check logging of folder events + t.Run("Log public folder event", func(t *testing.T) { + defer solaceContainer.StopOnError() + + // Create the queue and topic subscription in solace + queueName := "folder-event-public-legacy" + assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) + assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern)) + + topicName := "topic://audit-log/eu01/v1/resource-manager/folder-created" + assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName)) + + // Instantiate audit api + auditApi, err := NewDynamicLegacyAuditApi( + messagingApi, + validator, + ) + assert.NoError(t, err) + + // Instantiate test data + event, objectIdentifier := NewFolderAuditEvent(nil) + + // Log the event to solace + visibility := auditV1.Visibility_VISIBILITY_PUBLIC + ctx := context.WithValue(ctx, ContextKeyTopic, topicName) + assert.NoError(t, (*auditApi).LogWithTrace( + ctx, + event, + visibility, + NewRoutableIdentifier(objectIdentifier), + &traceParent, + &traceState, + )) + + message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) + assert.NoError(t, err) + + validateSentMessage(t, topicName, message, event, &traceParent, &traceState) + }) + + t.Run("Log private folder event", func(t *testing.T) { + defer solaceContainer.StopOnError() + + // Create the queue and topic subscription in solace + queueName := "folder-event-private-legacy" + assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) + assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern)) + + topicName := "topic://audit-log/eu01/v1/resource-manager/folder-created" + assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName)) + + // Instantiate audit api + auditApi, err := NewDynamicLegacyAuditApi( + messagingApi, + validator, + ) + assert.NoError(t, err) + + // Instantiate test data + event, objectIdentifier := NewFolderAuditEvent(nil) + + // Log the event to solace + visibility := auditV1.Visibility_VISIBILITY_PRIVATE + ctx := context.WithValue(ctx, ContextKeyTopic, topicName) + assert.NoError(t, (*auditApi).LogWithTrace( + ctx, + event, + visibility, + NewRoutableIdentifier(objectIdentifier), + &traceParent, + &traceState, + )) + + message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) + assert.NoError(t, err) + + validateSentMessage(t, topicName, message, event, &traceParent, &traceState) + }) + + // Check logging of project events + t.Run("Log public project event", func(t *testing.T) { + defer solaceContainer.StopOnError() + + // Create the queue and topic subscription in solace + queueName := "project-event-public-legacy" + assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) + assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern)) + + topicName := "topic://audit-log/eu01/v1/resource-manager/project-created" + assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName)) + + // Instantiate audit api + auditApi, err := NewDynamicLegacyAuditApi( + messagingApi, + validator, + ) + assert.NoError(t, err) + + // Instantiate test data + event, objectIdentifier := NewProjectAuditEvent(nil) + + // Log the event to solace + visibility := auditV1.Visibility_VISIBILITY_PUBLIC + ctx := context.WithValue(ctx, ContextKeyTopic, topicName) + assert.NoError(t, (*auditApi).LogWithTrace( + ctx, + event, + visibility, + NewRoutableIdentifier(objectIdentifier), + &traceParent, + &traceState, + )) + + message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) + assert.NoError(t, err) + + validateSentMessage(t, topicName, message, event, &traceParent, &traceState) + }) + + t.Run("Log private project event", func(t *testing.T) { + defer solaceContainer.StopOnError() + + // Create the queue and topic subscription in solace + queueName := "project-event-private-legacy" + assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) + assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern)) + + topicName := "topic://audit-log/eu01/v1/resource-manager/project-created" + assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName)) + + // Instantiate audit api + auditApi, err := NewDynamicLegacyAuditApi( + messagingApi, + validator, + ) + assert.NoError(t, err) + + // Instantiate test data + event, objectIdentifier := NewProjectAuditEvent(nil) + + // Log the event to solace + visibility := auditV1.Visibility_VISIBILITY_PRIVATE + ctx := context.WithValue(ctx, ContextKeyTopic, topicName) + assert.NoError(t, (*auditApi).LogWithTrace( + ctx, + event, + visibility, + NewRoutableIdentifier(objectIdentifier), + &traceParent, + &traceState, + )) + + message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) + assert.NoError(t, err) + + validateSentMessage(t, topicName, message, event, &traceParent, &traceState) + }) + + // Check logging of system events + t.Run("Log private system event", func(t *testing.T) { + defer solaceContainer.StopOnError() + + queueName := "system-event-private" + assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) + assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern)) + + topicName := "topic://audit-log/eu01/v1/resource-manager/system-changed" + assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName)) + + // Instantiate audit api + auditApi, err := NewDynamicLegacyAuditApi( + messagingApi, + validator, + ) + assert.NoError(t, err) + + // Instantiate test data + event := NewSystemAuditEvent(nil) + + // Log the event to solace + visibility := auditV1.Visibility_VISIBILITY_PRIVATE + ctx := context.WithValue(ctx, ContextKeyTopic, topicName) + assert.NoError(t, + (*auditApi).LogWithTrace( + ctx, + event, + visibility, + RoutableSystemIdentifier, + nil, + nil, + )) + + // Receive the event from solace + message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) + assert.NoError(t, err) + + // Check topic name + assert.Equal(t, topicName, *message.Properties.To) + assert.Nil(t, message.ApplicationProperties["cloudEvents:traceparent"]) + assert.Nil(t, message.ApplicationProperties["cloudEvents:tracestate"]) + + // Check deserialized message + var auditEvent LegacyAuditEvent + assert.NoError(t, json.Unmarshal(message.Data[0], &auditEvent)) + + assert.Equal(t, event.ProtoPayload.OperationName, auditEvent.EventName) + assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(), auditEvent.EventTimeStamp) + assert.Equal(t, event.ProtoPayload.AuthenticationInfo.PrincipalId, auditEvent.Initiator.Id) + assert.Equal(t, "SYSTEM_EVENT", auditEvent.EventType) + assert.Equal(t, "INFO", auditEvent.Severity) + assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Path, auditEvent.Request.Endpoint) + assert.Equal(t, event.ProtoPayload.RequestMetadata.CallerIp, auditEvent.SourceIpAddress) + assert.Equal(t, event.ProtoPayload.RequestMetadata.CallerSuppliedUserAgent, auditEvent.UserAgent) + }) + + t.Run("Log event with details", func(t *testing.T) { + defer solaceContainer.StopOnError() + + // Create the queue and topic subscription in solace + queueName := "org-event-with-details-legacy" + assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) + assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern)) + + topicName := "topic://audit-log/eu01/v1/resource-manager/organization-created" + assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName)) + + // Instantiate audit api + auditApi, err := NewDynamicLegacyAuditApi( + messagingApi, + validator, + ) + assert.NoError(t, err) + + // Instantiate test data + event, objectIdentifier := NewOrganizationAuditEvent(nil) + + // Log the event to solace + visibility := auditV1.Visibility_VISIBILITY_PUBLIC + ctx := context.WithValue(ctx, ContextKeyTopic, topicName) + assert.NoError(t, (*auditApi).LogWithTrace( + ctx, + event, + visibility, + NewRoutableIdentifier(objectIdentifier), + &traceParent, + &traceState, + )) + + message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) + assert.NoError(t, err) + + validateSentMessageWithDetails(t, topicName, message, event, &traceParent, &traceState) + }) +} + +func TestDynamicLegacyAuditApi_NewLegacyAuditApi_MessagingApiNil(t *testing.T) { + auditApi, err := NewDynamicLegacyAuditApi(nil, nil) + assert.Nil(t, auditApi) + assert.EqualError(t, err, "messaging api nil") +} + +func TestDynamicLegacyAuditApi_ValidateAndSerialize_ValidationFailed(t *testing.T) { + expectedError := errors.New("expected error") + + validator := &ProtobufValidatorMock{} + validator.On("Validate", mock.Anything).Return(expectedError) + var protobufValidator ProtobufValidator = validator + + auditApi := DynamicLegacyAuditApi{validator: &protobufValidator} + + event := NewSystemAuditEvent(nil) + _, err := auditApi.ValidateAndSerialize(event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier) + assert.ErrorIs(t, err, expectedError) +} + +func TestDynamicLegacyAuditApi_Log_ValidationFailed(t *testing.T) { + expectedError := errors.New("expected error") + + validator := &ProtobufValidatorMock{} + validator.On("Validate", mock.Anything).Return(expectedError) + var protobufValidator ProtobufValidator = validator + + auditApi := DynamicLegacyAuditApi{validator: &protobufValidator} + + event := NewSystemAuditEvent(nil) + err := auditApi.Log(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier) + assert.ErrorIs(t, err, expectedError) +} + +func TestDynamicLegacyAuditApi_Log_NilEvent(t *testing.T) { + auditApi := DynamicLegacyAuditApi{} + err := auditApi.Log(context.Background(), nil, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier) + assert.ErrorIs(t, err, ErrEventNil) +} + +func TestDynamicLegacyAuditApi_ConvertAndSerializeIntoLegacyFormatInvalidObjectIdentifierType(t *testing.T) { + customization := func(event *auditV1.AuditLogEntry, + objectIdentifier *auditV1.ObjectIdentifier) { + objectIdentifier.Type = "invalid" + } + event, objectIdentifier := NewProjectAuditEvent(&customization) + + validator := &ProtobufValidatorMock{} + validator.On("Validate", mock.Anything).Return(nil) + var protobufValidator ProtobufValidator = validator + + auditApi := DynamicLegacyAuditApi{validator: &protobufValidator} + _, err := auditApi.ValidateAndSerialize(event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier)) + assert.ErrorIs(t, err, ErrUnsupportedRoutableType) +} diff --git a/audit/api/api_legacy_test.go b/audit/api/api_legacy_test.go index 7268052..e56b74b 100644 --- a/audit/api/api_legacy_test.go +++ b/audit/api/api_legacy_test.go @@ -48,8 +48,6 @@ func TestLegacyAuditApi(t *testing.T) { t.Run("Log public organization event", func(t *testing.T) { defer solaceContainer.StopOnError() - slog.Info("test abc") - // Create the queue and topic subscription in solace queueName := "org-event-public-legacy" assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) @@ -548,17 +546,3 @@ func TestLegacyAuditApi_ConvertAndSerializeIntoLegacyFormatInvalidObjectIdentifi _, err := auditApi.ValidateAndSerialize(event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier)) assert.ErrorIs(t, err, ErrUnsupportedRoutableType) } - -func TestLegacyAuditApi_ConvertAndSerializeIntoLegacyFormat_NoObjectIdentifier(t *testing.T) { - event, _ := NewProjectAuditEvent(nil) - routableEvent := auditV1.RoutableAuditEvent{ - OperationName: event.ProtoPayload.OperationName, - Visibility: auditV1.Visibility_VISIBILITY_PUBLIC, - ObjectIdentifier: nil, - Data: nil, - } - - auditApi := LegacyAuditApi{} - _, err := auditApi.convertAndSerializeIntoLegacyFormat(event, &routableEvent) - assert.ErrorIs(t, err, ErrObjectIdentifierNil) -} diff --git a/proto/audit/v1/audit_event.proto b/proto/audit/v1/audit_event.proto index 63c893f..25684b6 100644 --- a/proto/audit/v1/audit_event.proto +++ b/proto/audit/v1/audit_event.proto @@ -408,6 +408,7 @@ message AttributeContext { // "email": "max@mail.schwarz", // "iss": "https://api.dev.stackit.cloud", // "jti": "45a196e0-480f-4c34-a592-dc5db81c8c3a" + // "sub": "cd94f01a-df2e-4456-902f-48f5e57f0b63" // } // // Required: true