mirror of
https://dev.azure.com/schwarzit/schwarzit.stackit-public/_git/audit-go
synced 2026-02-20 14:41:51 +00:00
Rename module to dev.azure.com/schwarzit/schwarzit.stackit-core-platform/common-audit.git
This commit is contained in:
parent
de5d0a8948
commit
4437c7b510
16 changed files with 352 additions and 115 deletions
|
|
@ -1,8 +1,10 @@
|
||||||
package main
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
auditV1 "audit-schema/gen/go/audit/v1"
|
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/common-audit.git/gen/go/audit/v1"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
@ -41,14 +43,29 @@ type AuditApi interface {
|
||||||
- protovalidate.ValidationError - if schema validation errors have been detected
|
- protovalidate.ValidationError - if schema validation errors have been detected
|
||||||
- protobuf serialization errors - if the event couldn't be serialized
|
- protobuf serialization errors - if the event couldn't be serialized
|
||||||
*/
|
*/
|
||||||
Log(ctx context.Context, event *auditV1.AuditEvent, visibility auditV1.Visibility, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier) error
|
Log(
|
||||||
|
ctx context.Context,
|
||||||
|
event *auditV1.AuditEvent,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
routingIdentifier *RoutingIdentifier,
|
||||||
|
objectIdentifier *auditV1.ObjectIdentifier,
|
||||||
|
) error
|
||||||
|
|
||||||
// ValidateAndSerialize validates and serializes the event into a byte representation.
|
// ValidateAndSerialize validates and serializes the event into a byte representation.
|
||||||
// The result has to be sent explicitly by calling the Send method.
|
// The result has to be sent explicitly by calling the Send method.
|
||||||
ValidateAndSerialize(event *auditV1.AuditEvent, visibility auditV1.Visibility, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier) (SerializedPayload, error)
|
ValidateAndSerialize(
|
||||||
|
event *auditV1.AuditEvent,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
routingIdentifier *RoutingIdentifier,
|
||||||
|
objectIdentifier *auditV1.ObjectIdentifier,
|
||||||
|
) (SerializedPayload, error)
|
||||||
|
|
||||||
// Send the serialized content as byte array to the audit log system.
|
// Send the serialized content as byte array to the audit log system.
|
||||||
Send(ctx context.Context, routingIdentifier *RoutingIdentifier, serializedPayload *SerializedPayload) error
|
Send(
|
||||||
|
ctx context.Context,
|
||||||
|
routingIdentifier *RoutingIdentifier,
|
||||||
|
serializedPayload *SerializedPayload,
|
||||||
|
) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProtobufValidator is an abstraction for validators.
|
// ProtobufValidator is an abstraction for validators.
|
||||||
|
|
@ -81,3 +98,11 @@ type RoutingIdentifier struct {
|
||||||
Identifier uuid.UUID
|
Identifier uuid.UUID
|
||||||
Type RoutingIdentifierType
|
Type RoutingIdentifierType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TopicNameResolver is an abstraction for dynamic topic name resolution
|
||||||
|
// based on event data or api parameters.
|
||||||
|
type TopicNameResolver interface {
|
||||||
|
|
||||||
|
// Resolve returns a topic name for the given routing identifier
|
||||||
|
Resolve(routingIdentifier *RoutingIdentifier) (string, error)
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
package main
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
auditV1 "audit-schema/gen/go/audit/v1"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -45,7 +48,13 @@ var ErrMessagingApiNil = errors.New("messaging api nil")
|
||||||
// ErrSerializedPayloadNil states that the give serialized payload is nil
|
// ErrSerializedPayloadNil states that the give serialized payload is nil
|
||||||
var ErrSerializedPayloadNil = errors.New("serialized payload nil")
|
var ErrSerializedPayloadNil = errors.New("serialized payload nil")
|
||||||
|
|
||||||
func validateAndSerializePartially(validator *ProtobufValidator, event *auditV1.AuditEvent, visibility auditV1.Visibility, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier) (*auditV1.RoutableAuditEvent, error) {
|
func validateAndSerializePartially(
|
||||||
|
validator *ProtobufValidator,
|
||||||
|
event *auditV1.AuditEvent,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
routingIdentifier *RoutingIdentifier,
|
||||||
|
objectIdentifier *auditV1.ObjectIdentifier,
|
||||||
|
) (*auditV1.RoutableAuditEvent, error) {
|
||||||
|
|
||||||
// Return error if the given event is nil
|
// Return error if the given event is nil
|
||||||
if event == nil {
|
if event == nil {
|
||||||
|
|
@ -107,9 +116,11 @@ func validateAndSerializePartially(validator *ProtobufValidator, event *auditV1.
|
||||||
|
|
||||||
// Set oneof protobuf fields after creation of the object
|
// Set oneof protobuf fields after creation of the object
|
||||||
if objectIdentifier == nil {
|
if objectIdentifier == nil {
|
||||||
routableEvent.ResourceReference = &auditV1.RoutableAuditEvent_ObjectName{ObjectName: auditV1.ObjectName_OBJECT_NAME_SYSTEM}
|
routableEvent.ResourceReference = &auditV1.RoutableAuditEvent_ObjectName{
|
||||||
|
ObjectName: auditV1.ObjectName_OBJECT_NAME_SYSTEM}
|
||||||
} else {
|
} else {
|
||||||
routableEvent.ResourceReference = &auditV1.RoutableAuditEvent_ObjectIdentifier{ObjectIdentifier: objectIdentifier}
|
routableEvent.ResourceReference = &auditV1.RoutableAuditEvent_ObjectIdentifier{
|
||||||
|
ObjectIdentifier: objectIdentifier}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = (*validator).Validate(&routableEvent)
|
err = (*validator).Validate(&routableEvent)
|
||||||
|
|
@ -146,7 +157,13 @@ func serializeToProtobufMessage(routableEvent *auditV1.RoutableAuditEvent) (Seri
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send implements AuditApi.Send
|
// Send implements AuditApi.Send
|
||||||
func send(topicNameResolver *TopicNameResolver, messagingApi *MessagingApi, ctx context.Context, routingIdentifier *RoutingIdentifier, serializedPayload *SerializedPayload) error {
|
func send(
|
||||||
|
topicNameResolver *TopicNameResolver,
|
||||||
|
messagingApi *messaging.MessagingApi,
|
||||||
|
ctx context.Context,
|
||||||
|
routingIdentifier *RoutingIdentifier,
|
||||||
|
serializedPayload *SerializedPayload,
|
||||||
|
) error {
|
||||||
|
|
||||||
if topicNameResolver == nil {
|
if topicNameResolver == nil {
|
||||||
return ErrTopicNameResolverNil
|
return ErrTopicNameResolverNil
|
||||||
|
|
@ -1,18 +1,30 @@
|
||||||
package main
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
auditV1 "audit-schema/gen/go/audit/v1"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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/bufbuild/protovalidate-go"
|
"github.com/bufbuild/protovalidate-go"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type MessagingApiMock struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MessagingApiMock) Send(ctx context.Context, topic string, data []byte, contentType string) error {
|
||||||
|
args := m.Called(ctx, topic, data, contentType)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
type ProtobufValidatorMock struct {
|
type ProtobufValidatorMock struct {
|
||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
@ -42,7 +54,9 @@ func NewValidator(t *testing.T) ProtobufValidator {
|
||||||
func Test_ValidateAndSerializePartially_EventNil(t *testing.T) {
|
func Test_ValidateAndSerializePartially_EventNil(t *testing.T) {
|
||||||
validator := NewValidator(t)
|
validator := NewValidator(t)
|
||||||
|
|
||||||
_, err := validateAndSerializePartially(&validator, nil, auditV1.Visibility_VISIBILITY_PUBLIC, nil, nil)
|
_, err := validateAndSerializePartially(
|
||||||
|
&validator, nil, auditV1.Visibility_VISIBILITY_PUBLIC, nil, nil)
|
||||||
|
|
||||||
assert.ErrorIs(t, err, ErrEventNil)
|
assert.ErrorIs(t, err, ErrEventNil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,7 +66,9 @@ func Test_ValidateAndSerializePartially_AuditEventValidationFailed(t *testing.T)
|
||||||
event, routingIdentifier, objectIdentifier := NewOrganizationAuditEvent(nil)
|
event, routingIdentifier, objectIdentifier := NewOrganizationAuditEvent(nil)
|
||||||
event.EventName = ""
|
event.EventName = ""
|
||||||
|
|
||||||
_, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, routingIdentifier, objectIdentifier)
|
_, err := validateAndSerializePartially(
|
||||||
|
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, routingIdentifier, objectIdentifier)
|
||||||
|
|
||||||
assert.EqualError(t, err, "validation error:\n - event_name: value is required [required]")
|
assert.EqualError(t, err, "validation error:\n - event_name: value is required [required]")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,8 +76,8 @@ func Test_ValidateAndSerializePartially_RoutableEventValidationFailed(t *testing
|
||||||
validator := NewValidator(t)
|
validator := NewValidator(t)
|
||||||
|
|
||||||
event, routingIdentifier, objectIdentifier := NewOrganizationAuditEvent(nil)
|
event, routingIdentifier, objectIdentifier := NewOrganizationAuditEvent(nil)
|
||||||
|
|
||||||
_, err := validateAndSerializePartially(&validator, event, 3, routingIdentifier, objectIdentifier)
|
_, err := validateAndSerializePartially(&validator, event, 3, routingIdentifier, objectIdentifier)
|
||||||
|
|
||||||
assert.EqualError(t, err, "validation error:\n - visibility: value must be one of the defined enum values [enum.defined_only]")
|
assert.EqualError(t, err, "validation error:\n - visibility: value must be one of the defined enum values [enum.defined_only]")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,46 +87,62 @@ func Test_ValidateAndSerializePartially_CheckVisibility(t *testing.T) {
|
||||||
event, routingIdentifier, objectIdentifier := NewOrganizationAuditEvent(nil)
|
event, routingIdentifier, objectIdentifier := NewOrganizationAuditEvent(nil)
|
||||||
|
|
||||||
t.Run("Visibility public - object identifier nil - routing identifier nil", func(t *testing.T) {
|
t.Run("Visibility public - object identifier nil - routing identifier nil", func(t *testing.T) {
|
||||||
_, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, nil, nil)
|
_, err := validateAndSerializePartially(
|
||||||
|
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, nil, nil)
|
||||||
|
|
||||||
assert.ErrorIs(t, err, ErrObjectIdentifierVisibilityMismatch)
|
assert.ErrorIs(t, err, ErrObjectIdentifierVisibilityMismatch)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Visibility public - object identifier nil - routing identifier set", func(t *testing.T) {
|
t.Run("Visibility public - object identifier nil - routing identifier set", func(t *testing.T) {
|
||||||
_, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, routingIdentifier, nil)
|
_, err := validateAndSerializePartially(
|
||||||
|
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, routingIdentifier, nil)
|
||||||
|
|
||||||
assert.ErrorIs(t, err, ErrObjectIdentifierVisibilityMismatch)
|
assert.ErrorIs(t, err, ErrObjectIdentifierVisibilityMismatch)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Visibility public - object identifier set - routing identifier nil", func(t *testing.T) {
|
t.Run("Visibility public - object identifier set - routing identifier nil", func(t *testing.T) {
|
||||||
_, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, nil, objectIdentifier)
|
_, err := validateAndSerializePartially(
|
||||||
|
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, nil, objectIdentifier)
|
||||||
|
|
||||||
assert.ErrorIs(t, err, ErrRoutableIdentifierMissing)
|
assert.ErrorIs(t, err, ErrRoutableIdentifierMissing)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Visibility public - object identifier set - routing identifier set", func(t *testing.T) {
|
t.Run("Visibility public - object identifier set - routing identifier set", func(t *testing.T) {
|
||||||
routableEvent, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, routingIdentifier, objectIdentifier)
|
routableEvent, err := validateAndSerializePartially(
|
||||||
|
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, routingIdentifier, objectIdentifier)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, routableEvent)
|
assert.NotNil(t, routableEvent)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Visibility private - object identifier nil - routing identifier nil", func(t *testing.T) {
|
t.Run("Visibility private - object identifier nil - routing identifier nil", func(t *testing.T) {
|
||||||
routableEvent, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, nil, nil)
|
routableEvent, err := validateAndSerializePartially(
|
||||||
|
&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, nil, nil)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, routableEvent)
|
assert.NotNil(t, routableEvent)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Visibility private - object identifier nil - routing identifier set", func(t *testing.T) {
|
t.Run("Visibility private - object identifier nil - routing identifier set", func(t *testing.T) {
|
||||||
routableEvent, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, routingIdentifier, nil)
|
routableEvent, err := validateAndSerializePartially(
|
||||||
|
&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, routingIdentifier, nil)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, routableEvent)
|
assert.NotNil(t, routableEvent)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Visibility private - object identifier set - routing identifier nil", func(t *testing.T) {
|
t.Run("Visibility private - object identifier set - routing identifier nil", func(t *testing.T) {
|
||||||
routableEvent, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, nil, objectIdentifier)
|
routableEvent, err := validateAndSerializePartially(
|
||||||
|
&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, nil, objectIdentifier)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, routableEvent)
|
assert.NotNil(t, routableEvent)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Visibility private - object identifier set - routing identifier set", func(t *testing.T) {
|
t.Run("Visibility private - object identifier set - routing identifier set", func(t *testing.T) {
|
||||||
routableEvent, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, routingIdentifier, objectIdentifier)
|
routableEvent, err := validateAndSerializePartially(
|
||||||
|
&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, routingIdentifier, objectIdentifier)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, routableEvent)
|
assert.NotNil(t, routableEvent)
|
||||||
})
|
})
|
||||||
|
|
@ -122,7 +154,9 @@ func Test_ValidateAndSerializePartially_IdentifierTypeMismatch(t *testing.T) {
|
||||||
event, routingIdentifier, objectIdentifier := NewFolderAuditEvent(nil)
|
event, routingIdentifier, objectIdentifier := NewFolderAuditEvent(nil)
|
||||||
routingIdentifier.Type = RoutingIdentifierTypeProject
|
routingIdentifier.Type = RoutingIdentifierTypeProject
|
||||||
|
|
||||||
_, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, routingIdentifier, objectIdentifier)
|
_, err := validateAndSerializePartially(
|
||||||
|
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, routingIdentifier, objectIdentifier)
|
||||||
|
|
||||||
assert.ErrorIs(t, err, ErrRoutableIdentifierTypeMismatch)
|
assert.ErrorIs(t, err, ErrRoutableIdentifierTypeMismatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,7 +166,9 @@ func Test_ValidateAndSerializePartially_IdentifierMismatch(t *testing.T) {
|
||||||
event, routingIdentifier, objectIdentifier := NewProjectAuditEvent(nil)
|
event, routingIdentifier, objectIdentifier := NewProjectAuditEvent(nil)
|
||||||
routingIdentifier.Identifier = uuid.New()
|
routingIdentifier.Identifier = uuid.New()
|
||||||
|
|
||||||
_, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, routingIdentifier, objectIdentifier)
|
_, err := validateAndSerializePartially(
|
||||||
|
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, routingIdentifier, objectIdentifier)
|
||||||
|
|
||||||
assert.ErrorIs(t, err, ErrRoutableIdentifierMismatch)
|
assert.ErrorIs(t, err, ErrRoutableIdentifierMismatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,7 +177,9 @@ func Test_ValidateAndSerializePartially_SystemEvent(t *testing.T) {
|
||||||
|
|
||||||
event := NewSystemAuditEvent(nil)
|
event := NewSystemAuditEvent(nil)
|
||||||
|
|
||||||
routableEvent, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, nil, nil)
|
routableEvent, err := validateAndSerializePartially(
|
||||||
|
&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, nil, nil)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
switch reference := routableEvent.ResourceReference.(type) {
|
switch reference := routableEvent.ResourceReference.(type) {
|
||||||
|
|
@ -159,7 +197,8 @@ func Test_SerializeToProtobufMessage(t *testing.T) {
|
||||||
event, identifier, objectIdentifier := NewOrganizationAuditEventWithDetails()
|
event, identifier, objectIdentifier := NewOrganizationAuditEventWithDetails()
|
||||||
|
|
||||||
// Serialize to routable event
|
// Serialize to routable event
|
||||||
routableEvent, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, identifier, objectIdentifier)
|
routableEvent, err := validateAndSerializePartially(
|
||||||
|
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, identifier, objectIdentifier)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Serialize to protobuf message
|
// Serialize to protobuf message
|
||||||
|
|
@ -193,7 +232,7 @@ func Test_Send_TopicNameResolutionError(t *testing.T) {
|
||||||
|
|
||||||
var serializedPayload SerializedPayload = &routablePayload{}
|
var serializedPayload SerializedPayload = &routablePayload{}
|
||||||
|
|
||||||
var messagingApi MessagingApi = &AmqpMessagingApi{}
|
var messagingApi messaging.MessagingApi = &messaging.AmqpMessagingApi{}
|
||||||
err := send(&topicNameResolver, &messagingApi, context.Background(), nil, &serializedPayload)
|
err := send(&topicNameResolver, &messagingApi, context.Background(), nil, &serializedPayload)
|
||||||
assert.ErrorIs(t, err, expectedError)
|
assert.ErrorIs(t, err, expectedError)
|
||||||
}
|
}
|
||||||
|
|
@ -206,7 +245,7 @@ func Test_Send_MessagingApiNil(t *testing.T) {
|
||||||
|
|
||||||
func Test_Send_SerializedPayloadNil(t *testing.T) {
|
func Test_Send_SerializedPayloadNil(t *testing.T) {
|
||||||
var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: "test"}
|
var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: "test"}
|
||||||
var messagingApi MessagingApi = &AmqpMessagingApi{}
|
var messagingApi messaging.MessagingApi = &messaging.AmqpMessagingApi{}
|
||||||
err := send(&topicNameResolver, &messagingApi, context.Background(), nil, nil)
|
err := send(&topicNameResolver, &messagingApi, context.Background(), nil, nil)
|
||||||
assert.ErrorIs(t, err, ErrSerializedPayloadNil)
|
assert.ErrorIs(t, err, ErrSerializedPayloadNil)
|
||||||
}
|
}
|
||||||
|
|
@ -220,7 +259,7 @@ func Test_Send(t *testing.T) {
|
||||||
|
|
||||||
messagingApiMock := MessagingApiMock{}
|
messagingApiMock := MessagingApiMock{}
|
||||||
messagingApiMock.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
messagingApiMock.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||||
var messagingApi MessagingApi = &messagingApiMock
|
var messagingApi messaging.MessagingApi = &messagingApiMock
|
||||||
|
|
||||||
assert.NoError(t, send(&topicNameResolver, &messagingApi, context.Background(), nil, &serializedPayload))
|
assert.NoError(t, send(&topicNameResolver, &messagingApi, context.Background(), nil, &serializedPayload))
|
||||||
assert.True(t, messagingApiMock.AssertNumberOfCalls(t, "Send", 1))
|
assert.True(t, messagingApiMock.AssertNumberOfCalls(t, "Send", 1))
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
package main
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
auditV1 "audit-schema/gen/go/audit/v1"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
"time"
|
"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"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// legacyPayload implements SerializedPayload.
|
// legacyPayload implements SerializedPayload.
|
||||||
|
|
@ -44,7 +47,7 @@ type LegacyTopicNameConfig struct {
|
||||||
//
|
//
|
||||||
// Note: The implementation will be deprecated and replaced with the "routableAuditApi" once the new audit log routing is implemented
|
// Note: The implementation will be deprecated and replaced with the "routableAuditApi" once the new audit log routing is implemented
|
||||||
type LegacyAuditApi struct {
|
type LegacyAuditApi struct {
|
||||||
messagingApi *MessagingApi
|
messagingApi *messaging.MessagingApi
|
||||||
topicNameResolver *TopicNameResolver
|
topicNameResolver *TopicNameResolver
|
||||||
validator *ProtobufValidator
|
validator *ProtobufValidator
|
||||||
}
|
}
|
||||||
|
|
@ -52,7 +55,11 @@ type LegacyAuditApi struct {
|
||||||
// NewLegacyAuditApi can be used to initialize the audit log api with LegacyAuditLogConnectionDetails.
|
// NewLegacyAuditApi can be used to initialize the audit log api with LegacyAuditLogConnectionDetails.
|
||||||
//
|
//
|
||||||
// Note: The NewLegacyAuditApi method will be deprecated and replaced with "newRoutableAuditApi" once the new audit log routing is implemented
|
// Note: The NewLegacyAuditApi method will be deprecated and replaced with "newRoutableAuditApi" once the new audit log routing is implemented
|
||||||
func NewLegacyAuditApi(messagingApi *MessagingApi, topicNameConfig LegacyTopicNameConfig, validator ProtobufValidator) (*AuditApi, error) {
|
func NewLegacyAuditApi(
|
||||||
|
messagingApi *messaging.MessagingApi,
|
||||||
|
topicNameConfig LegacyTopicNameConfig,
|
||||||
|
validator ProtobufValidator,
|
||||||
|
) (*AuditApi, error) {
|
||||||
|
|
||||||
if messagingApi == nil {
|
if messagingApi == nil {
|
||||||
return nil, errors.New("messaging api nil")
|
return nil, errors.New("messaging api nil")
|
||||||
|
|
@ -72,7 +79,13 @@ func NewLegacyAuditApi(messagingApi *MessagingApi, topicNameConfig LegacyTopicNa
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log implements AuditApi.Log
|
// Log implements AuditApi.Log
|
||||||
func (a *LegacyAuditApi) Log(ctx context.Context, event *auditV1.AuditEvent, visibility auditV1.Visibility, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier) error {
|
func (a *LegacyAuditApi) Log(
|
||||||
|
ctx context.Context,
|
||||||
|
event *auditV1.AuditEvent,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
routingIdentifier *RoutingIdentifier,
|
||||||
|
objectIdentifier *auditV1.ObjectIdentifier,
|
||||||
|
) error {
|
||||||
|
|
||||||
serializedPayload, err := a.ValidateAndSerialize(event, visibility, routingIdentifier, objectIdentifier)
|
serializedPayload, err := a.ValidateAndSerialize(event, visibility, routingIdentifier, objectIdentifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -84,7 +97,13 @@ func (a *LegacyAuditApi) Log(ctx context.Context, event *auditV1.AuditEvent, vis
|
||||||
|
|
||||||
// ValidateAndSerialize implements AuditApi.ValidateAndSerialize.
|
// ValidateAndSerialize implements AuditApi.ValidateAndSerialize.
|
||||||
// It serializes the event into the byte representation of the legacy audit log system.
|
// It serializes the event into the byte representation of the legacy audit log system.
|
||||||
func (a *LegacyAuditApi) ValidateAndSerialize(event *auditV1.AuditEvent, visibility auditV1.Visibility, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier) (SerializedPayload, error) {
|
func (a *LegacyAuditApi) ValidateAndSerialize(
|
||||||
|
event *auditV1.AuditEvent,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
routingIdentifier *RoutingIdentifier,
|
||||||
|
objectIdentifier *auditV1.ObjectIdentifier,
|
||||||
|
) (SerializedPayload, error) {
|
||||||
|
|
||||||
routableEvent, err := validateAndSerializePartially(a.validator, event, visibility, routingIdentifier, objectIdentifier)
|
routableEvent, err := validateAndSerializePartially(a.validator, event, visibility, routingIdentifier, objectIdentifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -106,7 +125,12 @@ func (a *LegacyAuditApi) ValidateAndSerialize(event *auditV1.AuditEvent, visibil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send implements AuditApi.Send
|
// Send implements AuditApi.Send
|
||||||
func (a *LegacyAuditApi) Send(ctx context.Context, routingIdentifier *RoutingIdentifier, serializedPayload *SerializedPayload) error {
|
func (a *LegacyAuditApi) Send(
|
||||||
|
ctx context.Context,
|
||||||
|
routingIdentifier *RoutingIdentifier,
|
||||||
|
serializedPayload *SerializedPayload,
|
||||||
|
) error {
|
||||||
|
|
||||||
return send(a.topicNameResolver, a.messagingApi, ctx, routingIdentifier, serializedPayload)
|
return send(a.topicNameResolver, a.messagingApi, ctx, routingIdentifier, serializedPayload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,18 +1,21 @@
|
||||||
package main
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
auditV1 "audit-schema/gen/go/audit/v1"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/Azure/go-amqp"
|
|
||||||
"github.com/bufbuild/protovalidate-go"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLegacyAuditApi(t *testing.T) {
|
func TestLegacyAuditApi(t *testing.T) {
|
||||||
|
|
@ -23,12 +26,12 @@ func TestLegacyAuditApi(t *testing.T) {
|
||||||
defer cancelFn()
|
defer cancelFn()
|
||||||
|
|
||||||
// Start solace docker container
|
// Start solace docker container
|
||||||
solaceContainer, err := NewSolaceContainer(context.Background())
|
solaceContainer, err := messaging.NewSolaceContainer(context.Background())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
defer solaceContainer.Stop()
|
defer solaceContainer.Stop()
|
||||||
|
|
||||||
// Instantiate the messaging api
|
// Instantiate the messaging api
|
||||||
messagingApi, err := NewAmqpMessagingApi(AmqpConfig{URL: solaceContainer.AmqpConnectionString})
|
messagingApi, err := messaging.NewAmqpMessagingApi(messaging.AmqpConfig{URL: solaceContainer.AmqpConnectionString})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Validator
|
// Validator
|
||||||
|
|
@ -408,7 +411,7 @@ func validateSentMessageWithDetails(t *testing.T, topicName string, message *amq
|
||||||
assert.Equal(t, header.Value, (*auditEvent.Request.Headers)[header.Key])
|
assert.Equal(t, header.Value, (*auditEvent.Request.Headers)[header.Key])
|
||||||
}
|
}
|
||||||
|
|
||||||
for idx, _ := range event.Principals {
|
for idx := range event.Principals {
|
||||||
assert.Equal(t, event.Principals[idx].Id, auditEvent.ServiceAccountDelegationInfo.Principals[idx].Id)
|
assert.Equal(t, event.Principals[idx].Id, auditEvent.ServiceAccountDelegationInfo.Principals[idx].Id)
|
||||||
assert.Equal(t, event.Principals[idx].Email, auditEvent.ServiceAccountDelegationInfo.Principals[idx].Email)
|
assert.Equal(t, event.Principals[idx].Email, auditEvent.ServiceAccountDelegationInfo.Principals[idx].Email)
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
package main
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
auditV1 "audit-schema/gen/go/audit/v1"
|
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/common-audit.git/gen/go/audit/v1"
|
||||||
|
|
||||||
"github.com/bufbuild/protovalidate-go"
|
"github.com/bufbuild/protovalidate-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -36,7 +38,13 @@ func (a *MockAuditApi) Log(
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateAndSerialize implements AuditApi.ValidateAndSerialize
|
// ValidateAndSerialize implements AuditApi.ValidateAndSerialize
|
||||||
func (a *MockAuditApi) ValidateAndSerialize(event *auditV1.AuditEvent, visibility auditV1.Visibility, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier) (SerializedPayload, error) {
|
func (a *MockAuditApi) ValidateAndSerialize(
|
||||||
|
event *auditV1.AuditEvent,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
routingIdentifier *RoutingIdentifier,
|
||||||
|
objectIdentifier *auditV1.ObjectIdentifier,
|
||||||
|
) (SerializedPayload, error) {
|
||||||
|
|
||||||
routableEvent, err := validateAndSerializePartially(a.validator, event, visibility, routingIdentifier, objectIdentifier)
|
routableEvent, err := validateAndSerializePartially(a.validator, event, visibility, routingIdentifier, objectIdentifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
package main
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
auditV1 "audit-schema/gen/go/audit/v1"
|
|
||||||
"context"
|
"context"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/common-audit.git/gen/go/audit/v1"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMockAuditApi_Log(t *testing.T) {
|
func TestMockAuditApi_Log(t *testing.T) {
|
||||||
|
|
@ -17,25 +19,31 @@ func TestMockAuditApi_Log(t *testing.T) {
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
t.Run("Log", func(t *testing.T) {
|
t.Run("Log", func(t *testing.T) {
|
||||||
assert.Nil(t, (*auditApi).Log(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, routingIdentifier, objectIdentifier))
|
assert.Nil(t, (*auditApi).Log(
|
||||||
|
context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, routingIdentifier, objectIdentifier))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("ValidateAndSerialize", func(t *testing.T) {
|
t.Run("ValidateAndSerialize", func(t *testing.T) {
|
||||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||||
serializedPayload, err := (*auditApi).ValidateAndSerialize(event, visibility, routingIdentifier, objectIdentifier)
|
serializedPayload, err := (*auditApi).ValidateAndSerialize(
|
||||||
|
event, visibility, routingIdentifier, objectIdentifier)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
validateProtobufMessagePayload(t, serializedPayload.GetPayload(), routingIdentifier, objectIdentifier, event, event.EventName, visibility)
|
validateProtobufMessagePayload(
|
||||||
|
t, serializedPayload.GetPayload(), routingIdentifier, objectIdentifier, event, event.EventName, visibility)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("ValidateAndSerialize event nil", func(t *testing.T) {
|
t.Run("ValidateAndSerialize event nil", func(t *testing.T) {
|
||||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||||
_, err := (*auditApi).ValidateAndSerialize(nil, visibility, routingIdentifier, objectIdentifier)
|
_, err := (*auditApi).ValidateAndSerialize(nil, visibility, routingIdentifier, objectIdentifier)
|
||||||
|
|
||||||
assert.ErrorIs(t, err, ErrEventNil)
|
assert.ErrorIs(t, err, ErrEventNil)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Send", func(t *testing.T) {
|
t.Run("Send", func(t *testing.T) {
|
||||||
var payload SerializedPayload = &routablePayload{}
|
var payload SerializedPayload = &routablePayload{}
|
||||||
|
|
||||||
assert.Nil(t, (*auditApi).Send(context.Background(), routingIdentifier, &payload))
|
assert.Nil(t, (*auditApi).Send(context.Background(), routingIdentifier, &payload))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
package main
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
auditV1 "audit-schema/gen/go/audit/v1"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// routablePayload implements SerializedPayload.
|
// routablePayload implements SerializedPayload.
|
||||||
|
|
@ -34,9 +36,11 @@ type routableTopicNameResolver struct {
|
||||||
|
|
||||||
// Resolve implements TopicNameResolver.Resolve.
|
// Resolve implements TopicNameResolver.Resolve.
|
||||||
func (r *routableTopicNameResolver) Resolve(routingIdentifier *RoutingIdentifier) (string, error) {
|
func (r *routableTopicNameResolver) Resolve(routingIdentifier *RoutingIdentifier) (string, error) {
|
||||||
|
|
||||||
if routingIdentifier == nil {
|
if routingIdentifier == nil {
|
||||||
return r.systemTopicName, nil
|
return r.systemTopicName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
switch routingIdentifier.Type {
|
switch routingIdentifier.Type {
|
||||||
case RoutingIdentifierTypeOrganization:
|
case RoutingIdentifierTypeOrganization:
|
||||||
return fmt.Sprintf("topic://%s/%s", r.organizationTopicPrefix, routingIdentifier.Identifier), nil
|
return fmt.Sprintf("topic://%s/%s", r.organizationTopicPrefix, routingIdentifier.Identifier), nil
|
||||||
|
|
@ -58,13 +62,17 @@ type topicNameConfig struct {
|
||||||
// Warning: It is only there for local (compatibility) testing.
|
// Warning: It is only there for local (compatibility) testing.
|
||||||
// DO NOT USE IT!
|
// DO NOT USE IT!
|
||||||
type routableAuditApi struct {
|
type routableAuditApi struct {
|
||||||
messagingApi *MessagingApi
|
messagingApi *messaging.MessagingApi
|
||||||
topicNameResolver *TopicNameResolver
|
topicNameResolver *TopicNameResolver
|
||||||
validator *ProtobufValidator
|
validator *ProtobufValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRoutableAuditApi can be used to initialize the audit log api.
|
// NewRoutableAuditApi can be used to initialize the audit log api.
|
||||||
func newRoutableAuditApi(messagingApi *MessagingApi, topicNameConfig topicNameConfig, validator ProtobufValidator) (*AuditApi, error) {
|
func newRoutableAuditApi(
|
||||||
|
messagingApi *messaging.MessagingApi,
|
||||||
|
topicNameConfig topicNameConfig,
|
||||||
|
validator ProtobufValidator,
|
||||||
|
) (*AuditApi, error) {
|
||||||
|
|
||||||
if messagingApi == nil {
|
if messagingApi == nil {
|
||||||
return nil, errors.New("messaging api nil")
|
return nil, errors.New("messaging api nil")
|
||||||
|
|
@ -88,7 +96,13 @@ func newRoutableAuditApi(messagingApi *MessagingApi, topicNameConfig topicNameCo
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log implements AuditApi.Log
|
// Log implements AuditApi.Log
|
||||||
func (a *routableAuditApi) Log(ctx context.Context, event *auditV1.AuditEvent, visibility auditV1.Visibility, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier) error {
|
func (a *routableAuditApi) Log(
|
||||||
|
ctx context.Context,
|
||||||
|
event *auditV1.AuditEvent,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
routingIdentifier *RoutingIdentifier,
|
||||||
|
objectIdentifier *auditV1.ObjectIdentifier,
|
||||||
|
) error {
|
||||||
|
|
||||||
serializedPayload, err := a.ValidateAndSerialize(event, visibility, routingIdentifier, objectIdentifier)
|
serializedPayload, err := a.ValidateAndSerialize(event, visibility, routingIdentifier, objectIdentifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -99,8 +113,20 @@ func (a *routableAuditApi) Log(ctx context.Context, event *auditV1.AuditEvent, v
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateAndSerialize implements AuditApi.ValidateAndSerialize
|
// ValidateAndSerialize implements AuditApi.ValidateAndSerialize
|
||||||
func (a *routableAuditApi) ValidateAndSerialize(event *auditV1.AuditEvent, visibility auditV1.Visibility, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier) (SerializedPayload, error) {
|
func (a *routableAuditApi) ValidateAndSerialize(
|
||||||
routableEvent, err := validateAndSerializePartially(a.validator, event, visibility, routingIdentifier, objectIdentifier)
|
event *auditV1.AuditEvent,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
routingIdentifier *RoutingIdentifier,
|
||||||
|
objectIdentifier *auditV1.ObjectIdentifier,
|
||||||
|
) (SerializedPayload, error) {
|
||||||
|
|
||||||
|
routableEvent, err := validateAndSerializePartially(
|
||||||
|
a.validator,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
routingIdentifier,
|
||||||
|
objectIdentifier)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -109,6 +135,11 @@ func (a *routableAuditApi) ValidateAndSerialize(event *auditV1.AuditEvent, visib
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send implements AuditApi.Send
|
// Send implements AuditApi.Send
|
||||||
func (a *routableAuditApi) Send(ctx context.Context, routingIdentifier *RoutingIdentifier, serializedPayload *SerializedPayload) error {
|
func (a *routableAuditApi) Send(
|
||||||
|
ctx context.Context,
|
||||||
|
routingIdentifier *RoutingIdentifier,
|
||||||
|
serializedPayload *SerializedPayload,
|
||||||
|
) error {
|
||||||
|
|
||||||
return send(a.topicNameResolver, a.messagingApi, ctx, routingIdentifier, serializedPayload)
|
return send(a.topicNameResolver, a.messagingApi, ctx, routingIdentifier, serializedPayload)
|
||||||
}
|
}
|
||||||
|
|
@ -1,17 +1,20 @@
|
||||||
package main
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
auditV1 "audit-schema/gen/go/audit/v1"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"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/Azure/go-amqp"
|
||||||
"github.com/bufbuild/protovalidate-go"
|
"github.com/bufbuild/protovalidate-go"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRoutableAuditApi(t *testing.T) {
|
func TestRoutableAuditApi(t *testing.T) {
|
||||||
|
|
@ -21,12 +24,12 @@ func TestRoutableAuditApi(t *testing.T) {
|
||||||
defer cancelFn()
|
defer cancelFn()
|
||||||
|
|
||||||
// Start solace docker container
|
// Start solace docker container
|
||||||
solaceContainer, err := NewSolaceContainer(context.Background())
|
solaceContainer, err := messaging.NewSolaceContainer(context.Background())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
defer solaceContainer.Stop()
|
defer solaceContainer.Stop()
|
||||||
|
|
||||||
// Instantiate the messaging api
|
// Instantiate the messaging api
|
||||||
messagingApi, err := NewAmqpMessagingApi(AmqpConfig{URL: solaceContainer.AmqpConnectionString})
|
messagingApi, err := messaging.NewAmqpMessagingApi(messaging.AmqpConfig{URL: solaceContainer.AmqpConnectionString})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Validator
|
// Validator
|
||||||
|
|
@ -39,7 +42,10 @@ func TestRoutableAuditApi(t *testing.T) {
|
||||||
systemTopicName := "topic://system/admin-events"
|
systemTopicName := "topic://system/admin-events"
|
||||||
auditApi, err := newRoutableAuditApi(
|
auditApi, err := newRoutableAuditApi(
|
||||||
messagingApi,
|
messagingApi,
|
||||||
topicNameConfig{OrganizationTopicPrefix: organizationTopicPrefix, ProjectTopicPrefix: projectTopicPrefix, SystemTopicName: systemTopicName},
|
topicNameConfig{
|
||||||
|
OrganizationTopicPrefix: organizationTopicPrefix,
|
||||||
|
ProjectTopicPrefix: projectTopicPrefix,
|
||||||
|
SystemTopicName: systemTopicName},
|
||||||
validator,
|
validator,
|
||||||
)
|
)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
@ -69,7 +75,15 @@ func TestRoutableAuditApi(t *testing.T) {
|
||||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
validateSentEvent(t, organizationTopicPrefix, message, routingIdentifier, objectIdentifier, event, "ORGANIZATION_CREATED", visibility)
|
validateSentEvent(
|
||||||
|
t,
|
||||||
|
organizationTopicPrefix,
|
||||||
|
message,
|
||||||
|
routingIdentifier,
|
||||||
|
objectIdentifier,
|
||||||
|
event,
|
||||||
|
"ORGANIZATION_CREATED",
|
||||||
|
visibility)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Log private organization event", func(t *testing.T) {
|
t.Run("Log private organization event", func(t *testing.T) {
|
||||||
|
|
@ -81,7 +95,10 @@ func TestRoutableAuditApi(t *testing.T) {
|
||||||
|
|
||||||
// Instantiate test data
|
// Instantiate test data
|
||||||
event, routingIdentifier, objectIdentifier := NewOrganizationAuditEvent(nil)
|
event, routingIdentifier, objectIdentifier := NewOrganizationAuditEvent(nil)
|
||||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, fmt.Sprintf("org/%s", routingIdentifier.Identifier)))
|
topicName := fmt.Sprintf("org/%s", routingIdentifier.Identifier)
|
||||||
|
assert.NoError(
|
||||||
|
t,
|
||||||
|
solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicName))
|
||||||
|
|
||||||
// Log the event to solace
|
// Log the event to solace
|
||||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||||
|
|
@ -98,7 +115,15 @@ func TestRoutableAuditApi(t *testing.T) {
|
||||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
validateSentEvent(t, organizationTopicPrefix, message, routingIdentifier, objectIdentifier, event, "ORGANIZATION_CREATED", visibility)
|
validateSentEvent(
|
||||||
|
t,
|
||||||
|
organizationTopicPrefix,
|
||||||
|
message,
|
||||||
|
routingIdentifier,
|
||||||
|
objectIdentifier,
|
||||||
|
event,
|
||||||
|
"ORGANIZATION_CREATED",
|
||||||
|
visibility)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Check logging of folder events
|
// Check logging of folder events
|
||||||
|
|
@ -126,7 +151,15 @@ func TestRoutableAuditApi(t *testing.T) {
|
||||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
validateSentEvent(t, organizationTopicPrefix, message, routingIdentifier, objectIdentifier, event, "FOLDER_CREATED", visibility)
|
validateSentEvent(
|
||||||
|
t,
|
||||||
|
organizationTopicPrefix,
|
||||||
|
message,
|
||||||
|
routingIdentifier,
|
||||||
|
objectIdentifier,
|
||||||
|
event,
|
||||||
|
"FOLDER_CREATED",
|
||||||
|
visibility)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Log private folder event", func(t *testing.T) {
|
t.Run("Log private folder event", func(t *testing.T) {
|
||||||
|
|
@ -138,7 +171,8 @@ func TestRoutableAuditApi(t *testing.T) {
|
||||||
|
|
||||||
// Instantiate test data
|
// Instantiate test data
|
||||||
event, routingIdentifier, objectIdentifier := NewFolderAuditEvent(nil)
|
event, routingIdentifier, objectIdentifier := NewFolderAuditEvent(nil)
|
||||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, fmt.Sprintf("org/%s", routingIdentifier.Identifier)))
|
topicName := fmt.Sprintf("org/%s", routingIdentifier.Identifier)
|
||||||
|
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicName))
|
||||||
|
|
||||||
// Log the event to solace
|
// Log the event to solace
|
||||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||||
|
|
@ -155,7 +189,15 @@ func TestRoutableAuditApi(t *testing.T) {
|
||||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
validateSentEvent(t, organizationTopicPrefix, message, routingIdentifier, objectIdentifier, event, "FOLDER_CREATED", visibility)
|
validateSentEvent(
|
||||||
|
t,
|
||||||
|
organizationTopicPrefix,
|
||||||
|
message,
|
||||||
|
routingIdentifier,
|
||||||
|
objectIdentifier,
|
||||||
|
event,
|
||||||
|
"FOLDER_CREATED",
|
||||||
|
visibility)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Check logging of project events
|
// Check logging of project events
|
||||||
|
|
@ -184,7 +226,15 @@ func TestRoutableAuditApi(t *testing.T) {
|
||||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
validateSentEvent(t, projectTopicPrefix, message, routingIdentifier, objectIdentifier, event, "PROJECT_CREATED", visibility)
|
validateSentEvent(
|
||||||
|
t,
|
||||||
|
projectTopicPrefix,
|
||||||
|
message,
|
||||||
|
routingIdentifier,
|
||||||
|
objectIdentifier,
|
||||||
|
event,
|
||||||
|
"PROJECT_CREATED",
|
||||||
|
visibility)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Log private project event", func(t *testing.T) {
|
t.Run("Log private project event", func(t *testing.T) {
|
||||||
|
|
@ -212,7 +262,15 @@ func TestRoutableAuditApi(t *testing.T) {
|
||||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
validateSentEvent(t, projectTopicPrefix, message, routingIdentifier, objectIdentifier, event, "PROJECT_CREATED", visibility)
|
validateSentEvent(
|
||||||
|
t,
|
||||||
|
projectTopicPrefix,
|
||||||
|
message,
|
||||||
|
routingIdentifier,
|
||||||
|
objectIdentifier,
|
||||||
|
event,
|
||||||
|
"PROJECT_CREATED",
|
||||||
|
visibility)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Check logging of system events
|
// Check logging of system events
|
||||||
|
|
@ -249,7 +307,14 @@ func TestRoutableAuditApi(t *testing.T) {
|
||||||
assert.NoError(t, proto.Unmarshal(message.Data[0], &protobufMessage))
|
assert.NoError(t, proto.Unmarshal(message.Data[0], &protobufMessage))
|
||||||
assert.Equal(t, "audit.v1.RoutableAuditEvent", protobufMessage.ProtobufType)
|
assert.Equal(t, "audit.v1.RoutableAuditEvent", protobufMessage.ProtobufType)
|
||||||
|
|
||||||
validateProtobufMessagePayload(t, message.Data[0], nil, nil, event, "SYSTEM_CHANGED", visibility)
|
validateProtobufMessagePayload(
|
||||||
|
t,
|
||||||
|
message.Data[0],
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
event,
|
||||||
|
"SYSTEM_CHANGED",
|
||||||
|
visibility)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Check logging of organization events
|
// Check logging of organization events
|
||||||
|
|
@ -277,11 +342,28 @@ func TestRoutableAuditApi(t *testing.T) {
|
||||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
validateSentEvent(t, organizationTopicPrefix, message, routingIdentifier, objectIdentifier, event, "ORGANIZATION_CREATED", visibility)
|
validateSentEvent(
|
||||||
|
t,
|
||||||
|
organizationTopicPrefix,
|
||||||
|
message,
|
||||||
|
routingIdentifier,
|
||||||
|
objectIdentifier,
|
||||||
|
event,
|
||||||
|
"ORGANIZATION_CREATED",
|
||||||
|
visibility)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateSentEvent(t *testing.T, topicPrefix string, message *amqp.Message, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier, event *auditV1.AuditEvent, eventName string, visibility auditV1.Visibility) {
|
func validateSentEvent(
|
||||||
|
t *testing.T,
|
||||||
|
topicPrefix string,
|
||||||
|
message *amqp.Message,
|
||||||
|
routingIdentifier *RoutingIdentifier,
|
||||||
|
objectIdentifier *auditV1.ObjectIdentifier,
|
||||||
|
event *auditV1.AuditEvent,
|
||||||
|
eventName string,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
) {
|
||||||
|
|
||||||
// Check topic name
|
// Check topic name
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
|
|
@ -293,10 +375,19 @@ func validateSentEvent(t *testing.T, topicPrefix string, message *amqp.Message,
|
||||||
assert.NoError(t, proto.Unmarshal(message.Data[0], &protobufMessage))
|
assert.NoError(t, proto.Unmarshal(message.Data[0], &protobufMessage))
|
||||||
assert.Equal(t, "audit.v1.RoutableAuditEvent", protobufMessage.ProtobufType)
|
assert.Equal(t, "audit.v1.RoutableAuditEvent", protobufMessage.ProtobufType)
|
||||||
|
|
||||||
validateProtobufMessagePayload(t, message.Data[0], routingIdentifier, objectIdentifier, event, eventName, visibility)
|
validateProtobufMessagePayload(
|
||||||
|
t, message.Data[0], routingIdentifier, objectIdentifier, event, eventName, visibility)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateProtobufMessagePayload(t *testing.T, payload []byte, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier, event *auditV1.AuditEvent, eventName string, visibility auditV1.Visibility) {
|
func validateProtobufMessagePayload(
|
||||||
|
t *testing.T,
|
||||||
|
payload []byte,
|
||||||
|
routingIdentifier *RoutingIdentifier,
|
||||||
|
objectIdentifier *auditV1.ObjectIdentifier,
|
||||||
|
event *auditV1.AuditEvent,
|
||||||
|
eventName string,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
) {
|
||||||
|
|
||||||
// Check deserialized message
|
// Check deserialized message
|
||||||
var protobufMessage auditV1.ProtobufMessage
|
var protobufMessage auditV1.ProtobufMessage
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
package main
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
auditV1 "audit-schema/gen/go/audit/v1"
|
"time"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/common-audit.git/gen/go/audit/v1"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"google.golang.org/protobuf/types/known/structpb"
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewOrganizationAuditEvent(
|
func NewOrganizationAuditEvent(
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package messaging
|
||||||
|
|
||||||
// terraform-provider-solacebroker
|
// terraform-provider-solacebroker
|
||||||
//
|
//
|
||||||
|
|
@ -23,12 +23,13 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/hashicorp/go-retryablehttp"
|
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-retryablehttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -172,7 +173,7 @@ func (c *Client) doRequest(request *http.Request) ([]byte, error) {
|
||||||
return rawBody, nil
|
return rawBody, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseResponseAsObject(ctx context.Context, request *http.Request, dataResponse []byte) (map[string]any, error) {
|
func parseResponseAsObject(_ context.Context, request *http.Request, dataResponse []byte) (map[string]any, error) {
|
||||||
data := map[string]any{}
|
data := map[string]any{}
|
||||||
err := json.Unmarshal(dataResponse, &data)
|
err := json.Unmarshal(dataResponse, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package messaging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -7,19 +7,12 @@ import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
//"dev.azure.com/schwarzit/schwarzit.stackit-core-platform/common-audit.git/audit/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default connection timeout for the AMQP connection
|
// Default connection timeout for the AMQP connection
|
||||||
const connectionTimeoutSeconds = 10
|
const connectionTimeoutSeconds = 10
|
||||||
|
|
||||||
// TopicNameResolver is an abstraction for dynamic topic name resolution
|
|
||||||
// based on event data or api parameters.
|
|
||||||
type TopicNameResolver interface {
|
|
||||||
|
|
||||||
// Resolve returns a topic name for the given routing identifier
|
|
||||||
Resolve(routingIdentifier *RoutingIdentifier) (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MessagingApi is an abstraction for a messaging system that can be used to send
|
// MessagingApi is an abstraction for a messaging system that can be used to send
|
||||||
// audit logs to the audit log system.
|
// audit logs to the audit log system.
|
||||||
type MessagingApi interface {
|
type MessagingApi interface {
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package messaging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -11,15 +11,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MessagingApiMock struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MessagingApiMock) Send(ctx context.Context, topic string, data []byte, contentType string) error {
|
|
||||||
args := m.Called(ctx, topic, data, contentType)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
type AmqpSessionMock struct {
|
type AmqpSessionMock struct {
|
||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package messaging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -229,10 +229,12 @@ func (c SolaceContainer) ValidateTopicName(topicSubscriptionTopicPattern string,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check topic subscription topic pattern
|
// Check topic subscription topic pattern
|
||||||
allowedTopicSubscriptionCharacters, err := regexp.Compile("(?:(?:[0-9A-Za-z-.]+|[0-9A-Za-z-.]*\\*)(?:/(?:[0-9A-Za-z-.]+|[0-9A-Za-z-.]*\\*))+|(?:[0-9A-Za-z-.]+|[0-9A-Za-z-.]*\\*)|/>)|>")
|
allowedTopicSubscriptionCharacters, err := regexp.Compile(
|
||||||
|
"(?:(?:[0-9A-Za-z-.]+|[0-9A-Za-z-.]*\\*)(?:/(?:[0-9A-Za-z-.]+|[0-9A-Za-z-.]*\\*))+|(?:[0-9A-Za-z-.]+|[0-9A-Za-z-.]*\\*)|/>)|>")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !allowedTopicSubscriptionCharacters.MatchString(topicSubscriptionTopicPattern) {
|
if !allowedTopicSubscriptionCharacters.MatchString(topicSubscriptionTopicPattern) {
|
||||||
return errors.New("invalid topic subscription name")
|
return errors.New("invalid topic subscription name")
|
||||||
}
|
}
|
||||||
2
go.mod
2
go.mod
|
|
@ -1,4 +1,4 @@
|
||||||
module audit-schema
|
module dev.azure.com/schwarzit/schwarzit.stackit-core-platform/common-audit.git
|
||||||
|
|
||||||
go 1.22
|
go 1.22
|
||||||
|
|
||||||
|
|
|
||||||
4
main.go
4
main.go
|
|
@ -1,8 +1,10 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
auditV1 "audit-schema/gen/go/audit/v1"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/common-audit.git/gen/go/audit/v1"
|
||||||
|
|
||||||
"github.com/bufbuild/protovalidate-go"
|
"github.com/bufbuild/protovalidate-go"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
"google.golang.org/protobuf/reflect/protoreflect"
|
"google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue