audit-go/pkg/audit/api/api_routable_test.go
Christian Schaible (EXT) 85aae1c2e7 Merged PR 779949: feat: Refactor module structure to reflect best practices
Security-concept-update-needed: false.

JIRA Work Item: STACKITALO-259
2025-05-19 11:54:00 +00:00

540 lines
18 KiB
Go

package api
import (
"context"
"errors"
"fmt"
"strings"
"testing"
"time"
"buf.build/go/protovalidate"
"github.com/Azure/go-amqp"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"go.opentelemetry.io/otel"
"google.golang.org/protobuf/proto"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
internalAuditApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/internal/audit/api"
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
pkgMessagingApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/api"
pgkMessagingCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/common"
pkgMessagingTest "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/test"
)
func TestRoutableAuditApi(t *testing.T) {
// Specify test timeout
ctx, cancelFn := context.WithTimeout(context.Background(), 120*time.Second)
defer cancelFn()
// Start solace docker container
solaceContainer, err := pkgMessagingTest.NewSolaceContainer(context.Background())
assert.NoError(t, err)
defer solaceContainer.Stop()
// Instantiate the messaging api
amqpApi, err := pkgMessagingApi.NewAmqpApi(pgkMessagingCommon.AmqpConnectionPoolConfig{
Parameters: pgkMessagingCommon.AmqpConnectionConfig{BrokerUrl: solaceContainer.AmqpConnectionString},
PoolSize: 1,
})
assert.NoError(t, err)
// Validator
validator, err := protovalidate.New()
assert.NoError(t, err)
// Instantiate the audit api
organizationTopicPrefix := "org"
projectTopicPrefix := "project"
folderTopicPrefix := "folder"
systemTopicName := "topic://system/admin-events"
auditApi, err := newRoutableAuditApi(
amqpApi,
topicNameConfig{
FolderTopicPrefix: folderTopicPrefix,
OrganizationTopicPrefix: organizationTopicPrefix,
ProjectTopicPrefix: projectTopicPrefix,
SystemTopicName: systemTopicName},
validator,
)
assert.NoError(t, err)
// Check that event-type data-access is rejected as it is currently
// not supported by downstream services
t.Run("reject data access event", func(t *testing.T) {
defer solaceContainer.StopOnError()
// Create the queue and topic subscription in solace
queueName := "org-reject-data-access"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "org/*"))
// Instantiate test data
event, objectIdentifier := internalAuditApi.NewOrganizationAuditEvent(nil)
event.LogName = strings.Replace(event.LogName, string(pkgAuditCommon.EventTypeAdminActivity), string(pkgAuditCommon.EventTypeDataAccess), 1)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
assert.ErrorIs(t, auditApi.Log(
ctx,
event,
visibility,
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)), pkgAuditCommon.ErrUnsupportedEventTypeDataAccess)
})
// 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"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "org/*"))
// Instantiate test data
event, objectIdentifier := internalAuditApi.NewOrganizationAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
assert.NoError(t, auditApi.Log(
ctx,
event,
visibility,
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)))
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentEvent(
t,
organizationTopicPrefix,
message,
objectIdentifier,
event,
"stackit.resourcemanager.v2.organization.created",
visibility)
})
t.Run("Log private organization event", func(t *testing.T) {
defer solaceContainer.StopOnError()
queueName := "org-event-private"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "org/*"))
// Instantiate test data
event, objectIdentifier := internalAuditApi.NewOrganizationAuditEvent(nil)
topicName := fmt.Sprintf("org/%s", objectIdentifier.Identifier)
assert.NoError(
t,
solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicName))
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
assert.NoError(t,
auditApi.Log(
ctx,
event,
visibility,
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)))
// Receive the event from solace
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentEvent(
t,
organizationTopicPrefix,
message,
objectIdentifier,
event,
"stackit.resourcemanager.v2.organization.created",
visibility)
})
// 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"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "folder/*"))
// Instantiate test data
event, objectIdentifier := internalAuditApi.NewFolderAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
assert.NoError(t, auditApi.Log(
ctx,
event,
visibility,
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)))
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentEvent(
t,
folderTopicPrefix,
message,
objectIdentifier,
event,
"stackit.resourcemanager.v2.folder.created",
visibility)
})
t.Run("Log private folder event", func(t *testing.T) {
defer solaceContainer.StopOnError()
queueName := "folder-event-private"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "folder/*"))
// Instantiate test data
event, objectIdentifier := internalAuditApi.NewFolderAuditEvent(nil)
topicName := fmt.Sprintf("folder/%s", objectIdentifier.Identifier)
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicName))
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
assert.NoError(t,
auditApi.Log(
ctx,
event,
visibility,
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)))
// Receive the event from solace
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentEvent(
t,
folderTopicPrefix,
message,
objectIdentifier,
event,
"stackit.resourcemanager.v2.folder.created",
visibility)
})
// Check logging of project events
t.Run("Log public project event", func(t *testing.T) {
defer solaceContainer.StopOnError()
queueName := "project-event-public"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "project/*"))
// Instantiate test data
event, objectIdentifier := internalAuditApi.NewProjectAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
assert.NoError(t,
auditApi.Log(
ctx,
event,
visibility,
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)))
// Receive the event from solace
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentEvent(
t,
projectTopicPrefix,
message,
objectIdentifier,
event,
"stackit.resourcemanager.v2.project.created",
visibility)
})
t.Run("Log private project event", func(t *testing.T) {
defer solaceContainer.StopOnError()
queueName := "project-event-private"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "project/*"))
// Instantiate test data
event, objectIdentifier := internalAuditApi.NewProjectAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
assert.NoError(t,
auditApi.Log(
ctx,
event,
visibility,
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)))
// Receive the event from solace
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentEvent(
t,
projectTopicPrefix,
message,
objectIdentifier,
event,
"stackit.resourcemanager.v2.project.created",
visibility)
})
// 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 := internalAuditApi.NewProjectSystemAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
assert.NoError(t,
auditApi.Log(
ctx,
event,
visibility,
pkgAuditCommon.RoutableSystemIdentifier,
))
// 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.Equal(t, "", applicationProperties["cloudEvents:traceparent"])
assert.Equal(t, "", applicationProperties["cloudEvents:tracestate"])
// Check deserialized message
validateRoutableEventPayload(
t,
message.Data[0],
pkgAuditCommon.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()
queueName := "system-event-private"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "system/*"))
// Instantiate test data
event := internalAuditApi.NewSystemAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
assert.NoError(t,
auditApi.Log(
ctx,
event,
visibility,
pkgAuditCommon.RoutableSystemIdentifier,
))
// 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.Equal(t, "", applicationProperties["cloudEvents:traceparent"])
assert.Equal(t, "", applicationProperties["cloudEvents:tracestate"])
// Check deserialized message
validateRoutableEventPayload(
t,
message.Data[0],
pkgAuditCommon.SystemIdentifier,
event,
"stackit.resourcemanager.v2.system.changed",
visibility)
})
// Check logging of organization events
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"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "org/*"))
// Instantiate test data
event, objectIdentifier := internalAuditApi.NewOrganizationAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
assert.NoError(t, auditApi.Log(
ctx,
event,
visibility,
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)))
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentEvent(
t,
organizationTopicPrefix,
message,
objectIdentifier,
event,
"stackit.resourcemanager.v2.organization.created",
visibility)
})
}
func validateSentEvent(
t *testing.T,
topicPrefix string,
message *amqp.Message,
objectIdentifier *auditV1.ObjectIdentifier,
event *auditV1.AuditLogEntry,
operationName string,
visibility auditV1.Visibility,
) {
// Check topic name
assert.Equal(t,
fmt.Sprintf("topic://%s/%s", topicPrefix, objectIdentifier.Identifier),
*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, pkgAuditCommon.ContentTypeCloudEventsProtobuf, applicationProperties["cloudEvents:datacontenttype"])
assert.Equal(t, "audit.v1.RoutableAuditEvent", applicationProperties["cloudEvents:type"])
assert.Equal(t, "", applicationProperties["cloudEvents:traceparent"])
assert.Equal(t, "", applicationProperties["cloudEvents:tracestate"])
// Check deserialized message
validateRoutableEventPayload(
t, message.Data[0], objectIdentifier, event, operationName, visibility)
}
func validateRoutableEventPayload(
t *testing.T,
payload []byte,
objectIdentifier *auditV1.ObjectIdentifier,
event *auditV1.AuditLogEntry,
operationName string,
visibility auditV1.Visibility,
) {
// Check routable audit event parameters
var routableAuditEvent auditV1.RoutableAuditEvent
assert.NoError(t, proto.Unmarshal(payload, &routableAuditEvent))
assert.Equal(t, operationName, routableAuditEvent.OperationName)
assert.Equal(t, visibility, routableAuditEvent.Visibility)
assert.True(t, proto.Equal(objectIdentifier, routableAuditEvent.ObjectIdentifier))
var auditEvent auditV1.AuditLogEntry
switch data := routableAuditEvent.Data.(type) {
case *auditV1.RoutableAuditEvent_UnencryptedData:
assert.NoError(t, proto.Unmarshal(data.UnencryptedData.Data, &auditEvent))
default:
assert.Fail(t, "Encrypted data not expected")
}
// Check audit event
assert.True(t, proto.Equal(event, &auditEvent))
}
func TestRoutableTopicNameResolver_Resolve_UnsupportedIdentifierType(t *testing.T) {
resolver := routableTopicNameResolver{}
_, err := resolver.Resolve(pkgAuditCommon.NewRoutableIdentifier(&auditV1.ObjectIdentifier{Type: "unsupported"}))
assert.ErrorIs(t, err, pkgAuditCommon.ErrUnsupportedObjectIdentifierType)
}
func TestNewRoutableAuditApi_NewRoutableAuditApi_MessagingApiNil(t *testing.T) {
auditApi, err := newRoutableAuditApi(nil, topicNameConfig{}, nil)
assert.Nil(t, auditApi)
assert.EqualError(t, err, "messaging api nil")
}
func TestRoutableAuditApi_ValidateAndSerialize_ValidationFailed(t *testing.T) {
expectedError := errors.New("expected error")
validator := &ProtobufValidatorMock{}
validator.On("Validate", mock.Anything).Return(expectedError)
var protobufValidator pkgAuditCommon.ProtobufValidator = validator
auditApi := routableAuditApi{
tracer: otel.Tracer("test"),
validator: protobufValidator,
}
event := internalAuditApi.NewSystemAuditEvent(nil)
_, err := auditApi.ValidateAndSerialize(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.RoutableSystemIdentifier)
assert.ErrorIs(t, err, expectedError)
}
func TestRoutableAuditApi_Log_ValidationFailed(t *testing.T) {
expectedError := errors.New("expected error")
validator := &ProtobufValidatorMock{}
validator.On("Validate", mock.Anything).Return(expectedError)
var protobufValidator pkgAuditCommon.ProtobufValidator = validator
auditApi := routableAuditApi{
tracer: otel.Tracer("test"),
validator: protobufValidator,
}
event := internalAuditApi.NewSystemAuditEvent(nil)
err := auditApi.Log(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.RoutableSystemIdentifier)
assert.ErrorIs(t, err, expectedError)
}
func TestRoutableAuditApi_Log_NilEvent(t *testing.T) {
auditApi := routableAuditApi{tracer: otel.Tracer("test")}
err := auditApi.Log(context.Background(), nil, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.RoutableSystemIdentifier)
assert.ErrorIs(t, err, pkgAuditCommon.ErrEventNil)
}