mirror of
https://dev.azure.com/schwarzit/schwarzit.stackit-public/_git/audit-go
synced 2026-02-08 00:57:24 +00:00
494 lines
14 KiB
Go
494 lines
14 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/google/uuid"
|
|
"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/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.AmqpConfig{URL: solaceContainer.AmqpConnectionString})
|
|
assert.NoError(t, err)
|
|
|
|
// Validator
|
|
validator, err := protovalidate.New()
|
|
assert.NoError(t, err)
|
|
|
|
traceParent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
|
|
traceState := "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"
|
|
|
|
// 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 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).LogWithTrace(
|
|
ctx,
|
|
event,
|
|
visibility,
|
|
NewRoutableIdentifier(objectIdentifier),
|
|
&traceParent,
|
|
&traceState,
|
|
))
|
|
|
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
|
assert.NoError(t, err)
|
|
|
|
validateSentEvent(
|
|
t,
|
|
organizationTopicPrefix,
|
|
message,
|
|
objectIdentifier,
|
|
event,
|
|
"stackit.resourcemanager.v2.organization.created",
|
|
visibility,
|
|
&traceParent,
|
|
&traceState)
|
|
})
|
|
|
|
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).LogWithTrace(
|
|
ctx,
|
|
event,
|
|
visibility,
|
|
NewRoutableIdentifier(objectIdentifier),
|
|
&traceParent,
|
|
&traceState,
|
|
))
|
|
|
|
// 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,
|
|
&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"
|
|
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).LogWithTrace(
|
|
ctx,
|
|
event,
|
|
visibility,
|
|
NewRoutableIdentifier(objectIdentifier),
|
|
&traceParent,
|
|
&traceState,
|
|
))
|
|
|
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
|
assert.NoError(t, err)
|
|
|
|
validateSentEvent(
|
|
t,
|
|
folderTopicPrefix,
|
|
message,
|
|
objectIdentifier,
|
|
event,
|
|
"stackit.resourcemanager.v2.folder.created",
|
|
visibility,
|
|
&traceParent,
|
|
&traceState)
|
|
})
|
|
|
|
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).LogWithTrace(
|
|
ctx,
|
|
event,
|
|
visibility,
|
|
NewRoutableIdentifier(objectIdentifier),
|
|
&traceParent,
|
|
&traceState,
|
|
))
|
|
|
|
// 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,
|
|
&traceParent,
|
|
&traceState)
|
|
})
|
|
|
|
// 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).LogWithTrace(
|
|
ctx,
|
|
event,
|
|
visibility,
|
|
NewRoutableIdentifier(objectIdentifier),
|
|
&traceParent,
|
|
&traceState,
|
|
))
|
|
|
|
// 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,
|
|
&traceParent,
|
|
&traceState)
|
|
})
|
|
|
|
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).LogWithTrace(
|
|
ctx,
|
|
event,
|
|
visibility,
|
|
NewRoutableIdentifier(objectIdentifier),
|
|
&traceParent,
|
|
&traceState,
|
|
))
|
|
|
|
// 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,
|
|
&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, "system/*"))
|
|
|
|
// Instantiate test data
|
|
event := NewSystemAuditEvent(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],
|
|
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).LogWithTrace(
|
|
ctx,
|
|
event,
|
|
visibility,
|
|
NewRoutableIdentifier(objectIdentifier),
|
|
&traceParent,
|
|
&traceState,
|
|
))
|
|
|
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
|
assert.NoError(t, err)
|
|
|
|
validateSentEvent(
|
|
t,
|
|
organizationTopicPrefix,
|
|
message,
|
|
objectIdentifier,
|
|
event,
|
|
"stackit.resourcemanager.v2.organization.created",
|
|
visibility,
|
|
&traceParent,
|
|
&traceState)
|
|
})
|
|
}
|
|
|
|
func validateSentEvent(
|
|
t *testing.T,
|
|
topicPrefix string,
|
|
message *amqp.Message,
|
|
objectIdentifier *auditV1.ObjectIdentifier,
|
|
event *auditV1.AuditLogEntry,
|
|
eventName string,
|
|
visibility auditV1.Visibility,
|
|
traceParent *string,
|
|
traceState *string,
|
|
) {
|
|
|
|
// 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, *traceParent, applicationProperties["cloudEvents:traceparent"])
|
|
assert.Equal(t, *traceState, applicationProperties["cloudEvents:tracestate"])
|
|
|
|
// Check deserialized message
|
|
validateRoutableEventPayload(
|
|
t, message.Data[0], objectIdentifier, event, eventName, visibility)
|
|
}
|
|
|
|
func validateRoutableEventPayload(
|
|
t *testing.T,
|
|
payload []byte,
|
|
objectIdentifier *auditV1.ObjectIdentifier,
|
|
event *auditV1.AuditLogEntry,
|
|
eventName string,
|
|
visibility auditV1.Visibility,
|
|
) {
|
|
|
|
// Check routable audit event parameters
|
|
var routableAuditEvent auditV1.RoutableAuditEvent
|
|
assert.NoError(t, proto.Unmarshal(payload, &routableAuditEvent))
|
|
|
|
assert.Equal(t, eventName, routableAuditEvent.EventName)
|
|
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{validator: &protobufValidator}
|
|
|
|
event := NewSystemAuditEvent(nil)
|
|
_, err := auditApi.ValidateAndSerialize(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{validator: &protobufValidator}
|
|
|
|
event := NewSystemAuditEvent(nil)
|
|
err := auditApi.LogWithTrace(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier, nil, nil)
|
|
assert.ErrorIs(t, err, expectedError)
|
|
}
|
|
|
|
func TestRoutableAuditApi_Log_NilEvent(t *testing.T) {
|
|
auditApi := routableAuditApi{}
|
|
err := auditApi.Log(context.Background(), nil, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
|
|
assert.ErrorIs(t, err, ErrEventNil)
|
|
}
|