mirror of
https://dev.azure.com/schwarzit/schwarzit.stackit-public/_git/audit-go
synced 2026-02-08 00:57:24 +00:00
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
537 lines
17 KiB
Go
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)
|
|
}
|