audit-go/audit/api/api_routable_test.go
Christian Schaible 5742604629 Merged PR 716929: feat: Replace AMQP connection management
So far the SDK provided a messaging API that was not thread-safe (i.e. goroutine-safe). Additionally the SDK provided a MutexAPI which made it thread-safe at the cost of removed concurrency possibilities. The changes implemented in this commit replace both implementations with a thread-safe connection pool based solution.

The api gateway is a SDK user that requires reliable high performance send capabilities with a limit amount of amqp connections. These changes in the PR try address their requirements by moving the responsibility of connection management into the SDK. From this change other SDK users will benefit as well.

Security-concept-update-needed: false.

JIRA Work Item: STACKITALO-62
2025-01-27 13:23:54 +00:00

537 lines
17 KiB
Go

package api
import (
"context"
"errors"
"fmt"
"go.opentelemetry.io/otel"
"strings"
"testing"
"time"
"github.com/google/uuid"
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/messaging"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"github.com/Azure/go-amqp"
"github.com/bufbuild/protovalidate-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"google.golang.org/protobuf/proto"
)
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 := messaging.NewSolaceContainer(context.Background())
assert.NoError(t, err)
defer solaceContainer.Stop()
// Instantiate the messaging api
messagingApi, err := messaging.NewAmqpApi(messaging.AmqpConnectionPoolConfig{
Parameters: messaging.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(
messagingApi,
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 := newOrganizationAuditEvent(nil)
event.LogName = strings.Replace(event.LogName, string(EventTypeAdminActivity), string(EventTypeDataAccess), 1)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
assert.ErrorIs(t, auditApi.Log(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier)), 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 := newOrganizationAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
assert.NoError(t, auditApi.Log(
ctx,
event,
visibility,
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 := 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,
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 := newFolderAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
assert.NoError(t, auditApi.Log(
ctx,
event,
visibility,
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 := 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,
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 := newProjectAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
assert.NoError(t,
auditApi.Log(
ctx,
event,
visibility,
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 := newProjectAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
assert.NoError(t,
auditApi.Log(
ctx,
event,
visibility,
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 := newProjectSystemAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
assert.NoError(t,
auditApi.Log(
ctx,
event,
visibility,
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],
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 := newSystemAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
assert.NoError(t,
auditApi.Log(
ctx,
event,
visibility,
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],
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 := newOrganizationAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
assert.NoError(t, auditApi.Log(
ctx,
event,
visibility,
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, 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(NewRoutableIdentifier(&auditV1.ObjectIdentifier{Type: "unsupported"}))
assert.ErrorIs(t, err, 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 ProtobufValidator = validator
auditApi := routableAuditApi{
tracer: otel.Tracer("test"),
validator: protobufValidator,
}
event := newSystemAuditEvent(nil)
_, err := auditApi.ValidateAndSerialize(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, 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 ProtobufValidator = validator
auditApi := routableAuditApi{
tracer: otel.Tracer("test"),
validator: protobufValidator,
}
event := newSystemAuditEvent(nil)
err := auditApi.Log(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, 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, RoutableSystemIdentifier)
assert.ErrorIs(t, err, ErrEventNil)
}