diff --git a/audit/api/api_common.go b/audit/api/api_common.go index 43458d3..95224b2 100644 --- a/audit/api/api_common.go +++ b/audit/api/api_common.go @@ -18,18 +18,25 @@ import ( const ContentTypeCloudEventsProtobuf = "application/cloudevents+protobuf" const ContentTypeCloudEventsJson = "application/cloudevents+json; charset=UTF-8" -// ErrUnknownPluralType indicates that the given input is an unknown plural type -var ErrUnknownPluralType = errors.New("unknown plural type") +// ErrAttributeIdentifierInvalid indicates that the object identifier +// and the identifier in the checked attribute do not match +var ErrAttributeIdentifierInvalid = errors.New("attribute identifier invalid") -// ErrUnknownSingularType indicates that the given input is an unknown singular type -var ErrUnknownSingularType = errors.New("unknown singular type") +// ErrAttributeTypeInvalid indicates that an invalid type has been provided. +var ErrAttributeTypeInvalid = errors.New("attribute type invalid") -// ErrUnsupportedRoutableType indicates that the given input is an unsupported routable type -var ErrUnsupportedRoutableType = errors.New("unsupported routable type") +// 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") @@ -40,28 +47,24 @@ var ErrObjectIdentifierNil = errors.New("object identifier is nil") // * Visibility: Private, ObjectIdentifier: var ErrObjectIdentifierVisibilityMismatch = errors.New("object reference visibility mismatch") -// ErrUnsupportedObjectIdentifierType indicates that an unsupported object identifier type has been provided. -var ErrUnsupportedObjectIdentifierType = errors.New("unsupported object identifier type") - -// ErrAttributeTypeInvalid indicates that an invalid type has been provided. -var ErrAttributeTypeInvalid = errors.New("attribute type invalid") - -// ErrAttributeIdentifierInvalid indicates that the object identifier -// and the identifier in the checked attribute do not match -var ErrAttributeIdentifierInvalid = errors.New("attribute identifier invalid") - // ErrTopicNameResolverNil states that the topic name resolve is nil var ErrTopicNameResolverNil = errors.New("topic name resolver nil") -// ErrMessagingApiNil states that the messaging api is nil -var ErrMessagingApiNil = errors.New("messaging api nil") +// ErrUnknownPluralType indicates that the given input is an unknown plural type +var ErrUnknownPluralType = errors.New("unknown plural type") -// ErrCloudEventNil states that the given cloud event is nil -var ErrCloudEventNil = errors.New("cloud event nil") +// ErrUnknownSingularType indicates that the given input is an unknown singular type +var ErrUnknownSingularType = errors.New("unknown singular 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, @@ -97,11 +100,18 @@ func validateAndSerializePartially( } // Check identifier consistency across event attributes - if err := areIdentifiersIdentical(routableIdentifier, event.LogName); err != nil { - return nil, err - } - if err := areIdentifiersIdentical(routableIdentifier, event.ProtoPayload.ResourceName); err != nil { - return nil, err + if strings.HasSuffix(event.LogName, string(EventTypeSystemEvent)) { + if !(routableIdentifier.Identifier == SystemIdentifier.Identifier && routableIdentifier.Type == SingularTypeSystem) { + return nil, ErrInvalidRoutableIdentifierForSystemEvent + } + // The resource name can either contain the system identifier or another resource identifier + } else { + if err := areIdentifiersIdentical(routableIdentifier, event.LogName); err != nil { + return nil, err + } + if err := areIdentifiersIdentical(routableIdentifier, event.ProtoPayload.ResourceName); err != nil { + return nil, err + } } // Test serialization even if the data is dropped later when logging to the legacy solution diff --git a/audit/api/api_common_test.go b/audit/api/api_common_test.go index e826181..30205e6 100644 --- a/audit/api/api_common_test.go +++ b/audit/api/api_common_test.go @@ -171,7 +171,7 @@ func Test_ValidateAndSerializePartially_CheckVisibility_SystemEvent(t *testing.T &validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier( &auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: string(SingularTypeOrganization)})) - assert.ErrorIs(t, err, ErrAttributeIdentifierInvalid) + assert.ErrorIs(t, err, ErrInvalidRoutableIdentifierForSystemEvent) }) t.Run("Visibility private - object identifier system", func(t *testing.T) { @@ -188,7 +188,7 @@ func Test_ValidateAndSerializePartially_CheckVisibility_SystemEvent(t *testing.T &validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, NewRoutableIdentifier( &auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: string(SingularTypeOrganization)})) - assert.ErrorIs(t, err, ErrAttributeIdentifierInvalid) + assert.ErrorIs(t, err, ErrInvalidRoutableIdentifierForSystemEvent) }) } diff --git a/audit/api/api_legacy_converter.go b/audit/api/api_legacy_converter.go index 5fc1a5c..43b086b 100644 --- a/audit/api/api_legacy_converter.go +++ b/audit/api/api_legacy_converter.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/url" + "strings" "time" auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/audit-go.git/gen/go/audit/v1" @@ -19,6 +20,20 @@ func convertAndSerializeIntoLegacyFormat( routableEvent *auditV1.RoutableAuditEvent, ) ([]byte, error) { + // Event type + var eventType string + if strings.HasSuffix(event.LogName, string(EventTypeAdminActivity)) { + eventType = "ADMIN_ACTIVITY" + } else if strings.HasSuffix(event.LogName, string(EventTypeSystemEvent)) { + eventType = "SYSTEM_EVENT" + } else if strings.HasSuffix(event.LogName, string(EventTypePolicyDenied)) { + eventType = "POLICY_DENIED" + } else if strings.HasSuffix(event.LogName, string(EventTypeDataAccess)) { + return nil, ErrUnsupportedEventTypeDataAccess + } else { + return nil, errors.New("unsupported event type") + } + // Source IP & User agent var sourceIpAddress string var userAgent string @@ -106,31 +121,26 @@ func convertAndSerializeIntoLegacyFormat( // 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 diff --git a/audit/api/api_legacy_dynamic_test.go b/audit/api/api_legacy_dynamic_test.go index c2c42ba..5328c3a 100644 --- a/audit/api/api_legacy_dynamic_test.go +++ b/audit/api/api_legacy_dynamic_test.go @@ -314,6 +314,64 @@ func TestDynamicLegacyAuditApi(t *testing.T) { validateSentMessage(t, topicName, message, event, &traceParent, &traceState) }) + // Check logging of system events with identifier + t.Run("Log private project system event", func(t *testing.T) { + defer solaceContainer.StopOnError() + + queueName := "project-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/project-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 := newProjectSystemAuditEvent(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.ResourceName, *auditEvent.ResourceName) + 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) + }) + // Check logging of system events t.Run("Log private system event", func(t *testing.T) { defer solaceContainer.StopOnError() diff --git a/audit/api/api_legacy_test.go b/audit/api/api_legacy_test.go index 4bf0428..83f19ee 100644 --- a/audit/api/api_legacy_test.go +++ b/audit/api/api_legacy_test.go @@ -315,6 +315,64 @@ func TestLegacyAuditApi(t *testing.T) { validateSentMessage(t, topicName, message, event, &traceParent, &traceState) }) + // Check logging of system events with identifier + t.Run("Log private project system event", func(t *testing.T) { + defer solaceContainer.StopOnError() + + queueName := "project-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/project-system-changed" + assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName)) + + // Instantiate audit api + auditApi, err := NewLegacyAuditApi( + messagingApi, + LegacyTopicNameConfig{TopicName: topicName}, + validator, + ) + assert.NoError(t, err) + + // Instantiate test data + event := newProjectSystemAuditEvent(nil) + + // Log the event to solace + visibility := auditV1.Visibility_VISIBILITY_PRIVATE + 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.ResourceName, *auditEvent.ResourceName) + 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) + }) + // Check logging of system events t.Run("Log private system event", func(t *testing.T) { defer solaceContainer.StopOnError() diff --git a/audit/api/api_routable_test.go b/audit/api/api_routable_test.go index 0c2c291..0804e3f 100644 --- a/audit/api/api_routable_test.go +++ b/audit/api/api_routable_test.go @@ -320,6 +320,58 @@ func TestRoutableAuditApi(t *testing.T) { &traceState) }) + // Check logging of system events with identifier + t.Run("Log private project system event", func(t *testing.T) { + defer solaceContainer.StopOnError() + + queueName := "project-system-event-private" + assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) + assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "system/*")) + + // Instantiate test data + event := newProjectSystemAuditEvent(nil) + + // Log the event to solace + visibility := auditV1.Visibility_VISIBILITY_PRIVATE + 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, systemTopicName, *message.Properties.To) + + // Check cloud event properties + applicationProperties := message.ApplicationProperties + assert.Equal(t, "1.0", applicationProperties["cloudEvents:specversion"]) + assert.Equal(t, "resource-manager", applicationProperties["cloudEvents:source"]) + _, isUuid := uuid.Parse(fmt.Sprintf("%s", applicationProperties["cloudEvents:id"])) + assert.True(t, true, isUuid) + assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime().UnixMilli(), applicationProperties["cloudEvents:time"]) + assert.Equal(t, "application/cloudevents+protobuf", applicationProperties["cloudEvents:datacontenttype"]) + assert.Equal(t, "audit.v1.RoutableAuditEvent", applicationProperties["cloudEvents:type"]) + assert.Nil(t, applicationProperties["cloudEvents:traceparent"]) + assert.Nil(t, applicationProperties["cloudEvents:tracestate"]) + + // Check deserialized message + validateRoutableEventPayload( + t, + message.Data[0], + RoutableSystemIdentifier.ToObjectIdentifier(), + event, + "stackit.resourcemanager.v2.system.changed", + visibility) + }) + // Check logging of system events t.Run("Log private system event", func(t *testing.T) { defer solaceContainer.StopOnError() diff --git a/audit/api/builder.go b/audit/api/builder.go index 68614d2..869a932 100644 --- a/audit/api/builder.go +++ b/audit/api/builder.go @@ -7,6 +7,7 @@ import ( "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/audit-go.git/log" "errors" "fmt" + "github.com/google/uuid" "go.opentelemetry.io/otel/trace" "time" ) @@ -38,13 +39,12 @@ type AuditParameters struct { } func getObjectIdAndTypeFromAuditParams( - ctx context.Context, auditParams *AuditParameters, -) (string, *SingularType, *PluralType, error) { +) (string, *PluralType, error) { objectId := auditParams.ObjectId if objectId == "" { - return "", nil, nil, errors.New("object id missing") + return "", nil, errors.New("object id missing") } var objectType *SingularType @@ -53,16 +53,16 @@ func getObjectIdAndTypeFromAuditParams( } if objectType == nil { - return "", nil, nil, errors.New("singular type missing") + return "", nil, errors.New("singular type missing") } // Convert to plural type plural, err := objectType.AsPluralType() if err != nil { log.AuditLogger.Error("failed to convert singular type to plural type", err) - return "", nil, nil, err + return "", nil, err } - return objectId, objectType, &plural, nil + return objectId, &plural, nil } // AuditLogEntryBuilder collects audit params to construct auditV1.AuditLogEntry @@ -324,7 +324,7 @@ func (builder *AuditLogEntryBuilder) Build(ctx context.Context, sequenceNumber S auditTime := time.Now() builder.auditMetadata.AuditTime = &auditTime - objectId, _, pluralType, err := getObjectIdAndTypeFromAuditParams(ctx, &builder.auditParams) + objectId, pluralType, err := getObjectIdAndTypeFromAuditParams(&builder.auditParams) if err != nil { return nil, err } @@ -340,8 +340,18 @@ func (builder *AuditLogEntryBuilder) Build(ctx context.Context, sequenceNumber S } resourceName := fmt.Sprintf("%s/%s", *pluralType, objectId) + var logIdentifier string + var logType PluralType + if builder.auditParams.EventType == EventTypeSystemEvent { + logIdentifier = SystemIdentifier.Identifier + logType = PluralTypeSystem + } else { + logIdentifier = objectId + logType = *pluralType + } + builder.auditMetadata.AuditInsertId = NewInsertId(time.Now().UTC(), builder.location, builder.workerId, uint64(sequenceNumber)) - builder.auditMetadata.AuditLogName = fmt.Sprintf("%s/%s/logs/%s", *pluralType, objectId, builder.auditParams.EventType) + builder.auditMetadata.AuditLogName = fmt.Sprintf("%s/%s/logs/%s", logType, logIdentifier, builder.auditParams.EventType) builder.auditMetadata.AuditResourceName = resourceName var details *map[string]interface{} = nil @@ -608,14 +618,29 @@ func (builder *AuditEventBuilder) Build(ctx context.Context, sequenceNumber Sequ if builder.auditLogEntryBuilder == nil { return nil, nil, "", fmt.Errorf("audit log entry builder not set") } + + objectId := builder.auditLogEntryBuilder.auditParams.ObjectId + objectType := builder.auditLogEntryBuilder.auditParams.ObjectType + var routingIdentifier *RoutableIdentifier + if builder.auditLogEntryBuilder.auditParams.EventType == EventTypeSystemEvent { + routingIdentifier = NewAuditRoutingIdentifier(uuid.Nil.String(), SingularTypeSystem) + if objectId == "" { + objectId = uuid.Nil.String() + builder.WithRequiredObjectId(objectId) + } + if objectType == "" { + objectType = SingularTypeSystem + builder.WithRequiredObjectType(objectType) + } + } else { + routingIdentifier = NewAuditRoutingIdentifier(objectId, objectType) + } + auditLogEntry, err := builder.auditLogEntryBuilder.Build(ctx, sequenceNumber) if err != nil { return nil, nil, "", err } - objectId := builder.auditLogEntryBuilder.auditParams.ObjectId - objectType := builder.auditLogEntryBuilder.auditParams.ObjectType - ctx, span := builder.tracer.Start(ctx, "create-audit-event") defer span.End() @@ -624,7 +649,6 @@ func (builder *AuditEventBuilder) Build(ctx context.Context, sequenceNumber Sequ var traceState *string = nil visibility := builder.visibility operation := auditLogEntry.ProtoPayload.OperationName - routingIdentifier := NewAuditRoutingIdentifier(objectId, objectType) // Validate and serialize the protobuf event into a cloud event _, validateSerializeSpan := builder.tracer.Start(ctx, "validate-and-serialize-audit-event") diff --git a/audit/api/builder_test.go b/audit/api/builder_test.go index cdd68d4..8cdba9b 100644 --- a/audit/api/builder_test.go +++ b/audit/api/builder_test.go @@ -20,50 +20,46 @@ func Test_getObjectIdAndTypeFromAuditParams(t *testing.T) { t.Run( "object id empty", func(t *testing.T) { - objectId, objectType, objectTypePlural, err := getObjectIdAndTypeFromAuditParams(context.Background(), &AuditParameters{}) + objectId, objectTypePlural, err := getObjectIdAndTypeFromAuditParams(&AuditParameters{}) assert.EqualError(t, err, "object id missing") assert.Equal(t, "", objectId) - assert.Nil(t, objectType) assert.Nil(t, objectTypePlural) }, ) t.Run( "object type empty", func(t *testing.T) { - objectId, objectType, objectTypePlural, err := getObjectIdAndTypeFromAuditParams(context.Background(), &AuditParameters{ObjectId: "value"}) + objectId, objectTypePlural, err := getObjectIdAndTypeFromAuditParams(&AuditParameters{ObjectId: "value"}) assert.EqualError(t, err, "singular type missing") assert.Equal(t, "", objectId) - assert.Nil(t, objectType) assert.Nil(t, objectTypePlural) }, ) t.Run( "object id and invalid type set", func(t *testing.T) { - objectId, objectType, objectTypePlural, err := getObjectIdAndTypeFromAuditParams( - context.Background(), &AuditParameters{ + objectId, objectTypePlural, err := getObjectIdAndTypeFromAuditParams( + &AuditParameters{ ObjectId: "value", ObjectType: AsSingularType("invalid"), }, ) assert.EqualError(t, err, "unknown singular type") assert.Equal(t, "", objectId) - assert.Nil(t, objectType) assert.Nil(t, objectTypePlural) }, ) t.Run( "object id and type set", func(t *testing.T) { - objectId, objectType, objectTypePlural, err := getObjectIdAndTypeFromAuditParams( - context.Background(), &AuditParameters{ + objectId, objectTypePlural, err := getObjectIdAndTypeFromAuditParams( + &AuditParameters{ ObjectId: "value", ObjectType: SingularTypeProject, }, ) assert.NoError(t, err) assert.Equal(t, "value", objectId) - assert.Equal(t, SingularTypeProject, *objectType) assert.Equal(t, PluralTypeProject, *objectTypePlural) }, ) @@ -329,7 +325,7 @@ func Test_AuditLogEntryBuilder(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, logEntry) - assert.Equal(t, "projects/1/logs/system-event", logEntry.LogName) + assert.Equal(t, fmt.Sprintf("system/%s/logs/system-event", uuid.Nil.String()), logEntry.LogName) assert.Nil(t, logEntry.Labels) assert.Nil(t, logEntry.TraceState) assert.Nil(t, logEntry.TraceParent) @@ -601,7 +597,8 @@ func Test_AuditEventBuilder(t *testing.T) { assert.NotNil(t, cloudEvent.Data) assert.NoError(t, proto.Unmarshal(cloudEvent.Data, &routableAuditEvent)) - assert.Equal(t, routableIdentifier.ToObjectIdentifier(), routableAuditEvent.ObjectIdentifier) + assert.Equal(t, routableIdentifier.ToObjectIdentifier().Identifier, routableAuditEvent.ObjectIdentifier.Identifier) + assert.Equal(t, routableIdentifier.ToObjectIdentifier().Type, routableAuditEvent.ObjectIdentifier.Type) assert.Equal(t, auditV1.Visibility_VISIBILITY_PUBLIC, routableAuditEvent.Visibility) assert.Equal(t, operation, routableAuditEvent.OperationName) @@ -714,7 +711,7 @@ func Test_AuditEventBuilder(t *testing.T) { WithAuditPermission(permission). WithAuditPermissionCheckResult(permissionCheckResult). WithDetails(details). - WithEventType(EventTypeSystemEvent). + WithEventType(EventTypeAdminActivity). WithLabels(map[string]string{"key": "label"}). WithNumResponseItems(int64(10)). WithRequestCorrelationId("correlationId"). @@ -751,7 +748,8 @@ func Test_AuditEventBuilder(t *testing.T) { assert.NotNil(t, cloudEvent.Data) assert.NoError(t, proto.Unmarshal(cloudEvent.Data, &routableAuditEvent)) - assert.Equal(t, routableIdentifier.ToObjectIdentifier(), routableAuditEvent.ObjectIdentifier) + assert.Equal(t, routableIdentifier.ToObjectIdentifier().Identifier, routableAuditEvent.ObjectIdentifier.Identifier) + assert.Equal(t, routableIdentifier.ToObjectIdentifier().Type, routableAuditEvent.ObjectIdentifier.Type) assert.Equal(t, auditV1.Visibility_VISIBILITY_PRIVATE, routableAuditEvent.Visibility) assert.Equal(t, operation, routableAuditEvent.OperationName) @@ -759,7 +757,7 @@ func Test_AuditEventBuilder(t *testing.T) { assert.NotNil(t, routableAuditEvent.GetUnencryptedData().Data) assert.NoError(t, proto.Unmarshal(routableAuditEvent.GetUnencryptedData().Data, &logEntry)) - assert.Equal(t, fmt.Sprintf("projects/%s/logs/system-event", objectId), logEntry.LogName) + assert.Equal(t, fmt.Sprintf("projects/%s/logs/admin-activity", objectId), logEntry.LogName) assert.Equal(t, map[string]string{"key": "label"}, logEntry.Labels) assert.Nil(t, logEntry.TraceState) assert.Nil(t, logEntry.TraceParent) @@ -836,14 +834,13 @@ func Test_AuditEventBuilder(t *testing.T) { assert.NoError(t, err) }) - t.Run("system event", func(t *testing.T) { + t.Run("system event with object reference", func(t *testing.T) { api, _ := NewMockAuditApi() sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator() tracer := otel.Tracer("test") objectId := uuid.NewString() operation := "stackit.demo-service.v1.operation" - routableIdentifier := RoutableIdentifier{Identifier: objectId, Type: SingularTypeProject} builder := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", "worker-id", "eu01"). WithRequiredObjectId(objectId). WithRequiredObjectType(SingularTypeProject). @@ -854,7 +851,8 @@ func Test_AuditEventBuilder(t *testing.T) { assert.NoError(t, err) assert.True(t, builder.IsBuilt()) - assert.Equal(t, &routableIdentifier, routingIdentifier) + assert.Equal(t, SystemIdentifier.Identifier, routingIdentifier.ToObjectIdentifier().Identifier) + assert.Equal(t, SystemIdentifier.Type, routingIdentifier.ToObjectIdentifier().Type) assert.Equal(t, operation, op) assert.NotNil(t, cloudEvent) @@ -872,7 +870,8 @@ func Test_AuditEventBuilder(t *testing.T) { assert.NotNil(t, cloudEvent.Data) assert.NoError(t, proto.Unmarshal(cloudEvent.Data, &routableAuditEvent)) - assert.Equal(t, routableIdentifier.ToObjectIdentifier(), routableAuditEvent.ObjectIdentifier) + assert.Equal(t, SystemIdentifier.Identifier, routableAuditEvent.ObjectIdentifier.Identifier) + assert.Equal(t, SystemIdentifier.Type, routableAuditEvent.ObjectIdentifier.Type) assert.Equal(t, auditV1.Visibility_VISIBILITY_PRIVATE, routableAuditEvent.Visibility) assert.Equal(t, operation, routableAuditEvent.OperationName) @@ -880,7 +879,7 @@ func Test_AuditEventBuilder(t *testing.T) { assert.NotNil(t, routableAuditEvent.GetUnencryptedData().Data) assert.NoError(t, proto.Unmarshal(routableAuditEvent.GetUnencryptedData().Data, &logEntry)) - assert.Equal(t, fmt.Sprintf("projects/%s/logs/system-event", objectId), logEntry.LogName) + assert.Equal(t, fmt.Sprintf("system/%s/logs/system-event", uuid.Nil.String()), logEntry.LogName) assert.Nil(t, logEntry.Labels) assert.Nil(t, logEntry.TraceState) assert.Nil(t, logEntry.TraceParent) @@ -951,6 +950,119 @@ func Test_AuditEventBuilder(t *testing.T) { assert.NoError(t, err) }) + t.Run("system event", func(t *testing.T) { + api, _ := NewMockAuditApi() + sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator() + tracer := otel.Tracer("test") + + operation := "stackit.demo-service.v1.operation" + builder := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", "worker-id", "eu01"). + WithRequiredOperation(operation). + AsSystemEvent() + + cloudEvent, routingIdentifier, op, err := builder.Build(context.Background(), SequenceNumber(1)) + assert.NoError(t, err) + assert.True(t, builder.IsBuilt()) + + assert.Equal(t, SystemIdentifier.Identifier, routingIdentifier.ToObjectIdentifier().Identifier) + assert.Equal(t, SystemIdentifier.Type, routingIdentifier.ToObjectIdentifier().Type) + assert.Equal(t, operation, op) + + assert.NotNil(t, cloudEvent) + assert.Equal(t, "application/cloudevents+protobuf", cloudEvent.DataContentType) + assert.Equal(t, "audit.v1.RoutableAuditEvent", cloudEvent.DataType) + assert.Regexp(t, "[0-9]+/eu01/worker-id/1", cloudEvent.Id) + assert.Equal(t, "demo-service", cloudEvent.Source) + assert.Equal(t, "1.0", cloudEvent.SpecVersion) + assert.Equal(t, fmt.Sprintf("system/%s", uuid.Nil.String()), cloudEvent.Subject) + assert.NotNil(t, cloudEvent.Time) + assert.Equal(t, "00-00000000000000000000000000000000-0000000000000000-00", *cloudEvent.TraceParent) + assert.Nil(t, cloudEvent.TraceState) + + var routableAuditEvent auditV1.RoutableAuditEvent + assert.NotNil(t, cloudEvent.Data) + assert.NoError(t, proto.Unmarshal(cloudEvent.Data, &routableAuditEvent)) + + assert.Equal(t, SystemIdentifier.Identifier, routableAuditEvent.ObjectIdentifier.Identifier) + assert.Equal(t, SystemIdentifier.Type, routableAuditEvent.ObjectIdentifier.Type) + assert.Equal(t, auditV1.Visibility_VISIBILITY_PRIVATE, routableAuditEvent.Visibility) + assert.Equal(t, operation, routableAuditEvent.OperationName) + + var logEntry auditV1.AuditLogEntry + assert.NotNil(t, routableAuditEvent.GetUnencryptedData().Data) + assert.NoError(t, proto.Unmarshal(routableAuditEvent.GetUnencryptedData().Data, &logEntry)) + + assert.Equal(t, fmt.Sprintf("system/%s/logs/system-event", uuid.Nil.String()), logEntry.LogName) + assert.Nil(t, logEntry.Labels) + assert.Nil(t, logEntry.TraceState) + assert.Nil(t, logEntry.TraceParent) + assert.Equal(t, auditV1.LogSeverity_LOG_SEVERITY_DEFAULT, logEntry.Severity) + assert.NotNil(t, logEntry.Timestamp) + assert.Nil(t, logEntry.CorrelationId) + assert.Regexp(t, "[0-9]+/eu01/worker-id/1", logEntry.InsertId) + + assert.NotNil(t, logEntry.ProtoPayload) + + authenticationInfo := logEntry.ProtoPayload.AuthenticationInfo + assert.NotNil(t, authenticationInfo) + assert.Equal(t, "do-not-reply@stackit.cloud", authenticationInfo.PrincipalEmail) + assert.Equal(t, "none", authenticationInfo.PrincipalId) + assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo) + assert.Nil(t, authenticationInfo.ServiceAccountName) + + assert.Nil(t, logEntry.ProtoPayload.AuthorizationInfo) + assert.Nil(t, logEntry.ProtoPayload.Metadata) + assert.Equal(t, operation, logEntry.ProtoPayload.OperationName) + assert.Nil(t, logEntry.ProtoPayload.Request) + + requestMetadata := logEntry.ProtoPayload.RequestMetadata + assert.NotNil(t, requestMetadata) + assert.Equal(t, "0.0.0.0", requestMetadata.CallerIp) + assert.Equal(t, "none", requestMetadata.CallerSuppliedUserAgent) + + requestAttributes := requestMetadata.RequestAttributes + assert.NotNil(t, requestAttributes) + assert.Equal(t, "none", requestAttributes.Path) + assert.NotNil(t, requestAttributes.Time) + assert.Equal(t, "0.0.0.0", requestAttributes.Host) + assert.Equal(t, auditV1.AttributeContext_HTTP_METHOD_OTHER, requestAttributes.Method) + assert.Nil(t, requestAttributes.Id) + assert.Equal(t, "none", requestAttributes.Scheme) + assert.Equal(t, map[string]string{"user-agent": "none"}, requestAttributes.Headers) + assert.Nil(t, requestAttributes.Query) + assert.Equal(t, "none", requestAttributes.Protocol) + + requestAttributesAuth := requestAttributes.Auth + assert.NotNil(t, requestAttributesAuth) + assert.Equal(t, "none/none", requestAttributesAuth.Principal) + assert.Nil(t, requestAttributesAuth.Audiences) + assert.NotNil(t, requestAttributesAuth.Claims) + assert.Equal(t, map[string]any{}, requestAttributesAuth.Claims.AsMap()) + + assert.Equal(t, fmt.Sprintf("system/%s", uuid.Nil.String()), logEntry.ProtoPayload.ResourceName) + assert.Nil(t, logEntry.ProtoPayload.Response) + + responseMetadata := logEntry.ProtoPayload.ResponseMetadata + assert.NotNil(t, responseMetadata) + assert.Nil(t, responseMetadata.ErrorDetails) + assert.Nil(t, responseMetadata.ErrorMessage) + assert.Equal(t, wrapperspb.Int32(200), responseMetadata.StatusCode) + + responseAttributes := responseMetadata.ResponseAttributes + assert.NotNil(t, responseAttributes) + assert.Nil(t, responseAttributes.Headers) + assert.Nil(t, responseAttributes.NumResponseItems) + assert.Nil(t, responseAttributes.Size) + assert.NotNil(t, responseAttributes.Time) + + assert.Equal(t, "demo-service", logEntry.ProtoPayload.ServiceName) + + validator, err := protovalidate.New() + assert.NoError(t, err) + err = validator.Validate(&logEntry) + assert.NoError(t, err) + }) + t.Run("with responsebody unserialized", func(t *testing.T) { api, _ := NewMockAuditApi() sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator() @@ -984,7 +1096,7 @@ func Test_AuditEventBuilder(t *testing.T) { WithAuditPermission(permission). WithAuditPermissionCheckResult(permissionCheckResult). WithDetails(details). - WithEventType(EventTypeSystemEvent). + WithEventType(EventTypeAdminActivity). WithLabels(map[string]string{"key": "label"}). WithNumResponseItems(int64(10)). WithRequestCorrelationId("correlationId"). diff --git a/audit/api/test_data.go b/audit/api/test_data.go index cc85c2e..0ff5db0 100644 --- a/audit/api/test_data.go +++ b/audit/api/test_data.go @@ -289,6 +289,88 @@ func newProjectAuditEvent( return auditEvent, objectIdentifier } +func newProjectSystemAuditEvent( + customization *func(*auditV1.AuditLogEntry)) *auditV1.AuditLogEntry { + + identifier := uuid.New() + requestId := fmt.Sprintf("%s/1", identifier) + claims, _ := structpb.NewStruct(map[string]interface{}{}) + correlationId := "9b5a8e9b-32a0-435f-b97b-a9a42b9e016b" + headers := make(map[string]string) + headers["Content-Type"] = "application/json" + labels := make(map[string]string) + labels["label1"] = "value1" + serviceAccountId := uuid.NewString() + serviceAccountName := fmt.Sprintf("projects/%s/service-accounts/%s", identifier, serviceAccountId) + delegationPrincipal := auditV1.ServiceAccountDelegationInfo{Authority: &auditV1.ServiceAccountDelegationInfo_SystemPrincipal_{}} + auditEvent := &auditV1.AuditLogEntry{ + LogName: fmt.Sprintf("%s/%s/logs/%s", SystemIdentifier.Type, SystemIdentifier.Identifier, EventTypeSystemEvent), + ProtoPayload: &auditV1.AuditLog{ + ServiceName: "resource-manager", + OperationName: "stackit.resourcemanager.v2.system.changed", + ResourceName: fmt.Sprintf("%s/%s", PluralTypeProject, identifier), + AuthenticationInfo: &auditV1.AuthenticationInfo{ + PrincipalId: serviceAccountId, + PrincipalEmail: "service-account@sa.stackit.cloud", + ServiceAccountName: &serviceAccountName, + ServiceAccountDelegationInfo: []*auditV1.ServiceAccountDelegationInfo{&delegationPrincipal}, + }, + AuthorizationInfo: []*auditV1.AuthorizationInfo{{ + Resource: fmt.Sprintf("%s/%s", PluralTypeProject, identifier), + Permission: nil, + Granted: nil, + }}, + RequestMetadata: &auditV1.RequestMetadata{ + CallerIp: "127.0.0.1", + CallerSuppliedUserAgent: "OpenAPI-Generator/ 1.0.0/ go", + RequestAttributes: &auditV1.AttributeContext_Request{ + Id: &requestId, + Method: auditV1.AttributeContext_HTTP_METHOD_POST, + Headers: headers, + Path: "/v2/projects", + Host: "stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud", + Scheme: "https", + Query: nil, + Time: timestamppb.New(time.Now().UTC()), + Protocol: "http/1.1", + Auth: &auditV1.AttributeContext_Auth{ + Principal: "https%3A%2F%2Faccounts.dev.stackit.cloud/stackit-resource-manager-dev", + Audiences: []string{"https:// stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud", "stackit", "api"}, + Claims: claims, + }, + }, + }, + Request: nil, + ResponseMetadata: &auditV1.ResponseMetadata{ + StatusCode: wrapperspb.Int32(200), + ErrorMessage: nil, + ErrorDetails: nil, + ResponseAttributes: &auditV1.AttributeContext_Response{ + NumResponseItems: nil, + Size: nil, + Headers: nil, + Time: timestamppb.New(time.Now().UTC()), + }, + }, + Response: nil, + Metadata: nil, + }, + InsertId: fmt.Sprintf("%d/eu01/e72182e8-0bb9-4be2-a19f-87fc0dd6e738/00000000001", time.Now().UnixNano()), + Labels: labels, + CorrelationId: &correlationId, + Timestamp: timestamppb.New(time.Now()), + Severity: auditV1.LogSeverity_LOG_SEVERITY_DEFAULT, + TraceParent: nil, + TraceState: nil, + } + + if customization != nil { + (*customization)(auditEvent) + } + + return auditEvent +} + func newSystemAuditEvent( customization *func(*auditV1.AuditLogEntry)) *auditV1.AuditLogEntry {