Introduce additional legacy api to dynamically set the topic name per message via context

This commit is contained in:
Christian Schaible 2024-08-20 12:58:24 +02:00
parent e759ae397a
commit a353d4fa0c
8 changed files with 897 additions and 302 deletions

View file

@ -201,7 +201,7 @@ type CloudEvent struct {
type TopicNameResolver interface { type TopicNameResolver interface {
// Resolve returns a topic name for the given object identifier // Resolve returns a topic name for the given object identifier
Resolve(objectIdentifier *RoutableIdentifier) (string, error) Resolve(routableIdentifier *RoutableIdentifier) (string, error)
} }
type RoutableIdentifier struct { type RoutableIdentifier struct {

View file

@ -2,20 +2,14 @@ package api
import ( import (
"context" "context"
"encoding/json"
"errors"
"fmt"
"net/url"
"time"
"dev.azure.com/schwarzit/schwarzit.stackit-core-platform/common-audit.git/audit/messaging" "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" auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/common-audit.git/gen/go/audit/v1"
"errors"
"fmt"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
var ErrUnsupportedSeverity = errors.New("unsupported severity level")
// LegacyTopicNameResolver implements TopicNameResolver. // LegacyTopicNameResolver implements TopicNameResolver.
// A hard-coded topic name is used, routing identifiers are ignored. // A hard-coded topic name is used, routing identifiers are ignored.
type LegacyTopicNameResolver struct { type LegacyTopicNameResolver struct {
@ -131,7 +125,7 @@ func (a *LegacyAuditApi) ValidateAndSerializeWithTrace(
} }
// Convert attributes // Convert attributes
legacyBytes, err := a.convertAndSerializeIntoLegacyFormat(event, routableEvent) legacyBytes, err := convertAndSerializeIntoLegacyFormat(event, routableEvent)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -160,279 +154,3 @@ func (a *LegacyAuditApi) Send(
return send(a.topicNameResolver, a.messagingApi, ctx, routableIdentifier, cloudEvent) return send(a.topicNameResolver, a.messagingApi, ctx, routableIdentifier, cloudEvent)
} }
// convertAndSerializeIntoLegacyFormat converts the protobuf events into the json serialized legacy audit log format
func (a *LegacyAuditApi) convertAndSerializeIntoLegacyFormat(
event *auditV1.AuditLogEntry,
routableEvent *auditV1.RoutableAuditEvent,
) ([]byte, error) {
// Source IP & User agent
var sourceIpAddress string
var userAgent string
if event.ProtoPayload == nil || event.ProtoPayload.RequestMetadata == nil {
sourceIpAddress = "0.0.0.0"
userAgent = "none"
} else {
sourceIpAddress = event.ProtoPayload.RequestMetadata.CallerIp
userAgent = event.ProtoPayload.RequestMetadata.CallerSuppliedUserAgent
}
// Principals
var serviceAccountDelegationInfo *LegacyAuditEventServiceAccountDelegationInfo = nil
if len(event.ProtoPayload.AuthenticationInfo.ServiceAccountDelegationInfo) > 0 {
var principals []LegacyAuditEventPrincipal
for _, principal := range event.ProtoPayload.AuthenticationInfo.ServiceAccountDelegationInfo {
switch principalValue := principal.Authority.(type) {
case *auditV1.ServiceAccountDelegationInfo_IdpPrincipal_:
principals = append(principals, LegacyAuditEventPrincipal{
Id: principalValue.IdpPrincipal.PrincipalId,
Email: &principalValue.IdpPrincipal.PrincipalEmail,
})
case *auditV1.ServiceAccountDelegationInfo_SystemPrincipal_:
principals = append(principals, LegacyAuditEventPrincipal{
Id: "system",
})
default:
return nil, errors.New("unsupported principal type")
}
}
serviceAccountDelegationInfo = &LegacyAuditEventServiceAccountDelegationInfo{Principals: principals}
}
var request LegacyAuditEventRequest
if event.ProtoPayload.RequestMetadata.RequestAttributes == nil {
request = LegacyAuditEventRequest{
Endpoint: "none",
}
} else {
var parameters map[string]interface{} = nil
if event.ProtoPayload.RequestMetadata.RequestAttributes.Path != "" &&
event.ProtoPayload.RequestMetadata.RequestAttributes.Query != nil &&
*event.ProtoPayload.RequestMetadata.RequestAttributes.Query != "" {
parameters = map[string]interface{}{}
parsedUrl, err := url.Parse(fmt.Sprintf("%s?%s",
event.ProtoPayload.RequestMetadata.RequestAttributes.Path,
*event.ProtoPayload.RequestMetadata.RequestAttributes.Query))
if err != nil {
return nil, err
}
for k, v := range parsedUrl.Query() {
parameters[k] = v
}
}
var body map[string]interface{} = nil
if event.ProtoPayload.Request != nil {
body = event.ProtoPayload.Request.AsMap()
}
var headers map[string]interface{} = nil
if event.ProtoPayload.RequestMetadata.RequestAttributes.Headers != nil {
headers = map[string]interface{}{}
for key, value := range event.ProtoPayload.RequestMetadata.RequestAttributes.Headers {
headers[key] = value
}
}
request = LegacyAuditEventRequest{
Endpoint: event.ProtoPayload.RequestMetadata.RequestAttributes.Path,
Parameters: &parameters,
Body: &body,
Headers: &headers,
}
}
if routableEvent.ObjectIdentifier == nil {
return nil, ErrObjectIdentifierNil
}
// Context and event type
var messageContext *LegacyAuditEventContext
var eventType string
switch routableEvent.ObjectIdentifier.Type {
case string(SingularTypeProject):
eventType = "ADMIN_ACTIVITY"
messageContext = &LegacyAuditEventContext{
OrganizationId: nil,
FolderId: nil,
ProjectId: &routableEvent.ObjectIdentifier.Identifier,
}
case string(SingularTypeFolder):
eventType = "ADMIN_ACTIVITY"
messageContext = &LegacyAuditEventContext{
OrganizationId: nil,
FolderId: &routableEvent.ObjectIdentifier.Identifier,
ProjectId: nil,
}
case string(SingularTypeOrganization):
eventType = "ADMIN_ACTIVITY"
messageContext = &LegacyAuditEventContext{
OrganizationId: &routableEvent.ObjectIdentifier.Identifier,
FolderId: nil,
ProjectId: nil,
}
case string(SingularTypeSystem):
eventType = "SYSTEM_EVENT"
messageContext = nil
default:
return nil, ErrUnsupportedObjectIdentifierType
}
var visibility string
switch routableEvent.Visibility {
case auditV1.Visibility_VISIBILITY_PUBLIC:
visibility = "PUBLIC"
case auditV1.Visibility_VISIBILITY_PRIVATE:
visibility = "PRIVATE"
}
// Details
var details = event.ProtoPayload.Request.AsMap()
// Result
var result = event.ProtoPayload.Response.AsMap()
// Severity
var severity string
switch event.Severity {
case auditV1.LogSeverity_LOG_SEVERITY_DEFAULT:
fallthrough
case auditV1.LogSeverity_LOG_SEVERITY_DEBUG:
fallthrough
case auditV1.LogSeverity_LOG_SEVERITY_INFO:
fallthrough
case auditV1.LogSeverity_LOG_SEVERITY_NOTICE:
fallthrough
case auditV1.LogSeverity_LOG_SEVERITY_WARNING:
severity = "INFO"
case auditV1.LogSeverity_LOG_SEVERITY_ERROR:
fallthrough
case auditV1.LogSeverity_LOG_SEVERITY_CRITICAL:
fallthrough
case auditV1.LogSeverity_LOG_SEVERITY_ALERT:
fallthrough
case auditV1.LogSeverity_LOG_SEVERITY_EMERGENCY:
severity = "ERROR"
default:
return nil, ErrUnsupportedSeverity
}
// Instantiate the legacy event - missing values are filled with defaults
legacyAuditEvent := LegacyAuditEvent{
Severity: severity,
Visibility: visibility,
EventType: eventType,
EventTimeStamp: event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(),
EventName: event.ProtoPayload.OperationName,
SourceIpAddress: sourceIpAddress,
UserAgent: userAgent,
Initiator: LegacyAuditEventPrincipal{
Id: event.ProtoPayload.AuthenticationInfo.PrincipalId,
Email: &event.ProtoPayload.AuthenticationInfo.PrincipalEmail,
},
ServiceAccountDelegationInfo: serviceAccountDelegationInfo,
Request: request,
Context: messageContext,
ResourceName: &event.ProtoPayload.ResourceName,
CorrelationId: event.CorrelationId,
Result: &result,
Details: &details,
}
bytes, err := json.Marshal(legacyAuditEvent)
if err != nil {
return nil, err
}
return bytes, nil
}
// LegacyAuditEvent has the format as follows:
/*
{
"severity": "INFO",
"visibility": "PUBLIC",
"eventType": "ADMIN_ACTIVITY",
"eventTimeStamp": "2019-08-24T14:15:22Z",
"eventName": "Create organization",
"sourceIpAddress": "127.0.0.1",
"userAgent": "CLI",
"initiator": {
"id": "string",
"email": "user@example.com"
},
"serviceAccountDelegationInfo": {
"principals": [
{
"id": "string",
"email": "user@example.com"
}
]
},
"request": {
"endpoint": "string",
"parameters": {},
"body": {},
"headers": {
"Content-Type": "application/json"
}
},
"context": {
"organizationId": "string",
"folderId": "string",
"projectId": "string"
},
"resourceId": "string",
"resourceName": "string",
"correlationId": "string",
"result": {},
"details": {}
}
*/
type LegacyAuditEvent struct {
Severity string `json:"severity"`
Visibility string `json:"visibility"`
EventType string `json:"eventType"`
EventTimeStamp time.Time `json:"eventTimeStamp"`
EventName string `json:"eventName"`
SourceIpAddress string `json:"sourceIpAddress"`
UserAgent string `json:"userAgent"`
Initiator LegacyAuditEventPrincipal `json:"initiator"`
ServiceAccountDelegationInfo *LegacyAuditEventServiceAccountDelegationInfo `json:"serviceAccountDelegationInfo"`
Request LegacyAuditEventRequest `json:"request"`
Context *LegacyAuditEventContext `json:"context"`
ResourceId *string `json:"resourceId"`
ResourceName *string `json:"resourceName"`
CorrelationId *string `json:"correlationId"`
Result *map[string]interface{} `json:"result"`
Details *map[string]interface{} `json:"details"`
}
// LegacyAuditEventPrincipal is a representation for a principal's id (+optional email) information.
type LegacyAuditEventPrincipal struct {
Id string `json:"id"`
Email *string `json:"email"`
}
// LegacyAuditEventServiceAccountDelegationInfo contains information about service account delegation.
type LegacyAuditEventServiceAccountDelegationInfo struct {
Principals []LegacyAuditEventPrincipal `json:"principals"`
}
// LegacyAuditEventRequest contains request information, which mirrors the action of the user and
// the resulting changes within the system.
type LegacyAuditEventRequest struct {
Endpoint string `json:"endpoint"`
Parameters *map[string]interface{} `json:"parameters"`
Body *map[string]interface{} `json:"body"`
Headers *map[string]interface{} `json:"headers"`
}
// LegacyAuditEventContext contains optional context information.
type LegacyAuditEventContext struct {
OrganizationId *string `json:"organizationId"`
FolderId *string `json:"folderId"`
ProjectId *string `json:"projectId"`
}

View file

@ -0,0 +1,288 @@
package api
import (
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/common-audit.git/gen/go/audit/v1"
"encoding/json"
"errors"
"fmt"
"net/url"
"time"
)
var ErrUnsupportedSeverity = errors.New("unsupported severity level")
// convertAndSerializeIntoLegacyFormat converts the protobuf events into the json serialized legacy audit log format
func convertAndSerializeIntoLegacyFormat(
event *auditV1.AuditLogEntry,
routableEvent *auditV1.RoutableAuditEvent,
) ([]byte, error) {
// Source IP & User agent
var sourceIpAddress string
var userAgent string
if event.ProtoPayload == nil || event.ProtoPayload.RequestMetadata == nil {
sourceIpAddress = "0.0.0.0"
userAgent = "none"
} else {
sourceIpAddress = event.ProtoPayload.RequestMetadata.CallerIp
userAgent = event.ProtoPayload.RequestMetadata.CallerSuppliedUserAgent
}
// Principals
var serviceAccountDelegationInfo *LegacyAuditEventServiceAccountDelegationInfo = nil
if len(event.ProtoPayload.AuthenticationInfo.ServiceAccountDelegationInfo) > 0 {
var principals []LegacyAuditEventPrincipal
for _, principal := range event.ProtoPayload.AuthenticationInfo.ServiceAccountDelegationInfo {
switch principalValue := principal.Authority.(type) {
case *auditV1.ServiceAccountDelegationInfo_IdpPrincipal_:
principals = append(principals, LegacyAuditEventPrincipal{
Id: principalValue.IdpPrincipal.PrincipalId,
Email: &principalValue.IdpPrincipal.PrincipalEmail,
})
case *auditV1.ServiceAccountDelegationInfo_SystemPrincipal_:
principals = append(principals, LegacyAuditEventPrincipal{
Id: "system",
})
default:
return nil, errors.New("unsupported principal type")
}
}
serviceAccountDelegationInfo = &LegacyAuditEventServiceAccountDelegationInfo{Principals: principals}
}
var request LegacyAuditEventRequest
if event.ProtoPayload.RequestMetadata.RequestAttributes == nil {
request = LegacyAuditEventRequest{
Endpoint: "none",
}
} else {
var parameters map[string]interface{} = nil
if event.ProtoPayload.RequestMetadata.RequestAttributes.Path != "" &&
event.ProtoPayload.RequestMetadata.RequestAttributes.Query != nil &&
*event.ProtoPayload.RequestMetadata.RequestAttributes.Query != "" {
parameters = map[string]interface{}{}
parsedUrl, err := url.Parse(fmt.Sprintf("%s?%s",
event.ProtoPayload.RequestMetadata.RequestAttributes.Path,
*event.ProtoPayload.RequestMetadata.RequestAttributes.Query))
if err != nil {
return nil, err
}
for k, v := range parsedUrl.Query() {
parameters[k] = v
}
}
var body map[string]interface{} = nil
if event.ProtoPayload.Request != nil {
body = event.ProtoPayload.Request.AsMap()
}
var headers map[string]interface{} = nil
if event.ProtoPayload.RequestMetadata.RequestAttributes.Headers != nil {
headers = map[string]interface{}{}
for key, value := range event.ProtoPayload.RequestMetadata.RequestAttributes.Headers {
headers[key] = value
}
}
request = LegacyAuditEventRequest{
Endpoint: event.ProtoPayload.RequestMetadata.RequestAttributes.Path,
Parameters: &parameters,
Body: &body,
Headers: &headers,
}
}
if routableEvent.ObjectIdentifier == nil {
return nil, ErrObjectIdentifierNil
}
// Context and event type
var messageContext *LegacyAuditEventContext
var eventType string
switch routableEvent.ObjectIdentifier.Type {
case string(SingularTypeProject):
eventType = "ADMIN_ACTIVITY"
messageContext = &LegacyAuditEventContext{
OrganizationId: nil,
FolderId: nil,
ProjectId: &routableEvent.ObjectIdentifier.Identifier,
}
case string(SingularTypeFolder):
eventType = "ADMIN_ACTIVITY"
messageContext = &LegacyAuditEventContext{
OrganizationId: nil,
FolderId: &routableEvent.ObjectIdentifier.Identifier,
ProjectId: nil,
}
case string(SingularTypeOrganization):
eventType = "ADMIN_ACTIVITY"
messageContext = &LegacyAuditEventContext{
OrganizationId: &routableEvent.ObjectIdentifier.Identifier,
FolderId: nil,
ProjectId: nil,
}
case string(SingularTypeSystem):
eventType = "SYSTEM_EVENT"
messageContext = nil
default:
return nil, ErrUnsupportedObjectIdentifierType
}
var visibility string
switch routableEvent.Visibility {
case auditV1.Visibility_VISIBILITY_PUBLIC:
visibility = "PUBLIC"
case auditV1.Visibility_VISIBILITY_PRIVATE:
visibility = "PRIVATE"
}
// Details
var details = event.ProtoPayload.Request.AsMap()
// Result
var result = event.ProtoPayload.Response.AsMap()
// Severity
var severity string
switch event.Severity {
case auditV1.LogSeverity_LOG_SEVERITY_DEFAULT:
fallthrough
case auditV1.LogSeverity_LOG_SEVERITY_DEBUG:
fallthrough
case auditV1.LogSeverity_LOG_SEVERITY_INFO:
fallthrough
case auditV1.LogSeverity_LOG_SEVERITY_NOTICE:
fallthrough
case auditV1.LogSeverity_LOG_SEVERITY_WARNING:
severity = "INFO"
case auditV1.LogSeverity_LOG_SEVERITY_ERROR:
fallthrough
case auditV1.LogSeverity_LOG_SEVERITY_CRITICAL:
fallthrough
case auditV1.LogSeverity_LOG_SEVERITY_ALERT:
fallthrough
case auditV1.LogSeverity_LOG_SEVERITY_EMERGENCY:
severity = "ERROR"
default:
return nil, ErrUnsupportedSeverity
}
// Instantiate the legacy event - missing values are filled with defaults
legacyAuditEvent := LegacyAuditEvent{
Severity: severity,
Visibility: visibility,
EventType: eventType,
EventTimeStamp: event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(),
EventName: event.ProtoPayload.OperationName,
SourceIpAddress: sourceIpAddress,
UserAgent: userAgent,
Initiator: LegacyAuditEventPrincipal{
Id: event.ProtoPayload.AuthenticationInfo.PrincipalId,
Email: &event.ProtoPayload.AuthenticationInfo.PrincipalEmail,
},
ServiceAccountDelegationInfo: serviceAccountDelegationInfo,
Request: request,
Context: messageContext,
ResourceName: &event.ProtoPayload.ResourceName,
CorrelationId: event.CorrelationId,
Result: &result,
Details: &details,
}
bytes, err := json.Marshal(legacyAuditEvent)
if err != nil {
return nil, err
}
return bytes, nil
}
// LegacyAuditEvent has the format as follows:
/*
{
"severity": "INFO",
"visibility": "PUBLIC",
"eventType": "ADMIN_ACTIVITY",
"eventTimeStamp": "2019-08-24T14:15:22Z",
"eventName": "Create organization",
"sourceIpAddress": "127.0.0.1",
"userAgent": "CLI",
"initiator": {
"id": "string",
"email": "user@example.com"
},
"serviceAccountDelegationInfo": {
"principals": [
{
"id": "string",
"email": "user@example.com"
}
]
},
"request": {
"endpoint": "string",
"parameters": {},
"body": {},
"headers": {
"Content-Type": "application/json"
}
},
"context": {
"organizationId": "string",
"folderId": "string",
"projectId": "string"
},
"resourceId": "string",
"resourceName": "string",
"correlationId": "string",
"result": {},
"details": {}
}
*/
type LegacyAuditEvent struct {
Severity string `json:"severity"`
Visibility string `json:"visibility"`
EventType string `json:"eventType"`
EventTimeStamp time.Time `json:"eventTimeStamp"`
EventName string `json:"eventName"`
SourceIpAddress string `json:"sourceIpAddress"`
UserAgent string `json:"userAgent"`
Initiator LegacyAuditEventPrincipal `json:"initiator"`
ServiceAccountDelegationInfo *LegacyAuditEventServiceAccountDelegationInfo `json:"serviceAccountDelegationInfo"`
Request LegacyAuditEventRequest `json:"request"`
Context *LegacyAuditEventContext `json:"context"`
ResourceId *string `json:"resourceId"`
ResourceName *string `json:"resourceName"`
CorrelationId *string `json:"correlationId"`
Result *map[string]interface{} `json:"result"`
Details *map[string]interface{} `json:"details"`
}
// LegacyAuditEventPrincipal is a representation for a principal's id (+optional email) information.
type LegacyAuditEventPrincipal struct {
Id string `json:"id"`
Email *string `json:"email"`
}
// LegacyAuditEventServiceAccountDelegationInfo contains information about service account delegation.
type LegacyAuditEventServiceAccountDelegationInfo struct {
Principals []LegacyAuditEventPrincipal `json:"principals"`
}
// LegacyAuditEventRequest contains request information, which mirrors the action of the user and
// the resulting changes within the system.
type LegacyAuditEventRequest struct {
Endpoint string `json:"endpoint"`
Parameters *map[string]interface{} `json:"parameters"`
Body *map[string]interface{} `json:"body"`
Headers *map[string]interface{} `json:"headers"`
}
// LegacyAuditEventContext contains optional context information.
type LegacyAuditEventContext struct {
OrganizationId *string `json:"organizationId"`
FolderId *string `json:"folderId"`
ProjectId *string `json:"projectId"`
}

View file

@ -0,0 +1,20 @@
package api
import (
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/common-audit.git/gen/go/audit/v1"
"github.com/stretchr/testify/assert"
"testing"
)
func Test_ConvertAndSerializeIntoLegacyFormat_NoObjectIdentifier(t *testing.T) {
event, _ := NewProjectAuditEvent(nil)
routableEvent := auditV1.RoutableAuditEvent{
OperationName: event.ProtoPayload.OperationName,
Visibility: auditV1.Visibility_VISIBILITY_PUBLIC,
ObjectIdentifier: nil,
Data: nil,
}
_, err := convertAndSerializeIntoLegacyFormat(event, &routableEvent)
assert.ErrorIs(t, err, ErrObjectIdentifierNil)
}

View file

@ -0,0 +1,152 @@
package api
import (
"context"
"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"
"errors"
"fmt"
"google.golang.org/protobuf/proto"
)
type ContextKey string
const ContextKeyTopic ContextKey = "topic"
var ErrNoTopicNameProvided = errors.New("no topic name provided")
var ErrTopicNameEmpty = errors.New("empty topic name provided")
// DynamicLegacyAuditApi is an implementation of AuditApi to send events to the legacy audit log system
// by setting the topic name explicitly in the context with the key "topic".
//
// Note: The implementation will be deprecated and replaced with the "routableAuditApi" once the new audit log routing is implemented
type DynamicLegacyAuditApi struct {
messagingApi *messaging.Api
validator *ProtobufValidator
}
// NewDynamicLegacyAuditApi can be used to initialize the audit log api.
//
// Note: The NewLegacyAuditApi method will be deprecated and replaced with "newRoutableAuditApi" once the new audit log routing is implemented
func NewDynamicLegacyAuditApi(
messagingApi *messaging.Api,
validator ProtobufValidator,
) (*AuditApi, error) {
if messagingApi == nil {
return nil, ErrMessagingApiNil
}
// Audit api
var auditApi AuditApi = &DynamicLegacyAuditApi{
messagingApi: messagingApi,
validator: &validator,
}
return &auditApi, nil
}
// Log implements AuditApi.Log
func (a *DynamicLegacyAuditApi) Log(
ctx context.Context,
event *auditV1.AuditLogEntry,
visibility auditV1.Visibility,
routableIdentifier *RoutableIdentifier,
) error {
return a.LogWithTrace(ctx, event, visibility, routableIdentifier, nil, nil)
}
// LogWithTrace implements AuditApi.LogWithTrace
func (a *DynamicLegacyAuditApi) LogWithTrace(
ctx context.Context,
event *auditV1.AuditLogEntry,
visibility auditV1.Visibility,
routableIdentifier *RoutableIdentifier,
traceParent *string,
traceState *string,
) error {
cloudEvent, err := a.ValidateAndSerializeWithTrace(event, visibility, routableIdentifier, traceParent, traceState)
if err != nil {
return err
}
return a.Send(ctx, routableIdentifier, cloudEvent)
}
// ValidateAndSerialize implements AuditApi.ValidateAndSerialize.
// It serializes the event into the byte representation of the legacy audit log system.
func (a *DynamicLegacyAuditApi) ValidateAndSerialize(
event *auditV1.AuditLogEntry,
visibility auditV1.Visibility,
routableIdentifier *RoutableIdentifier,
) (*CloudEvent, error) {
return a.ValidateAndSerializeWithTrace(event, visibility, routableIdentifier, nil, nil)
}
// ValidateAndSerializeWithTrace implements AuditApi.ValidateAndSerializeWithTrace.
// It serializes the event into the byte representation of the legacy audit log system.
func (a *DynamicLegacyAuditApi) ValidateAndSerializeWithTrace(
event *auditV1.AuditLogEntry,
visibility auditV1.Visibility,
routableIdentifier *RoutableIdentifier,
traceParent *string,
traceState *string,
) (*CloudEvent, error) {
routableEvent, err := validateAndSerializePartially(a.validator, event, visibility, routableIdentifier)
if err != nil {
return nil, err
}
// Do nothing with the serialized data in the legacy solution
_, err = proto.Marshal(routableEvent)
if err != nil {
return nil, err
}
// Convert attributes
legacyBytes, err := convertAndSerializeIntoLegacyFormat(event, routableEvent)
if err != nil {
return nil, err
}
message := CloudEvent{
SpecVersion: "1.0",
Source: event.ProtoPayload.ServiceName,
Id: event.InsertId,
Time: event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(),
DataContentType: ContentTypeCloudEventsProtobuf,
DataType: fmt.Sprintf("%v", routableEvent.ProtoReflect().Descriptor().FullName()),
Subject: event.ProtoPayload.ResourceName,
Data: legacyBytes,
TraceParent: traceParent,
TraceState: traceState,
}
return &message, nil
}
// Send implements AuditApi.Send
//
// Requires to have the topic name set as key "topic" in the context.
func (a *DynamicLegacyAuditApi) Send(
ctx context.Context,
routableIdentifier *RoutableIdentifier,
cloudEvent *CloudEvent,
) error {
rawTopicName := ctx.Value(ContextKeyTopic)
if rawTopicName == nil {
return ErrNoTopicNameProvided
}
topicName := fmt.Sprintf("%s", rawTopicName)
if len(topicName) == 0 {
return ErrTopicNameEmpty
}
var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: topicName}
return send(&topicNameResolver, a.messagingApi, ctx, routableIdentifier, cloudEvent)
}

View file

@ -0,0 +1,432 @@
package api
import (
"context"
"encoding/json"
"errors"
"log/slog"
"os"
"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/bufbuild/protovalidate-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestDynamicLegacyAuditApi(t *testing.T) {
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
// 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)
topicSubscriptionTopicPattern := "audit-log/>"
traceParent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
traceState := "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"
// 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-legacy"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
topicName := "topic://audit-log/eu01/v1/resource-manager/organization-created"
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
// Instantiate audit api
auditApi, err := NewDynamicLegacyAuditApi(
messagingApi,
validator,
)
assert.NoError(t, err)
// Instantiate test data
event, objectIdentifier := NewOrganizationAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
ctx := context.WithValue(ctx, ContextKeyTopic, topicName)
assert.NoError(t, (*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
))
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentMessage(t, topicName, message, event, &traceParent, &traceState)
})
t.Run("Log private organization event", func(t *testing.T) {
defer solaceContainer.StopOnError()
// Create the queue and topic subscription in solace
queueName := "org-event-private-legacy"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
topicName := "topic://audit-log/eu01/v1/resource-manager/organization-created"
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
// Instantiate audit api
auditApi, err := NewDynamicLegacyAuditApi(
messagingApi,
validator,
)
assert.NoError(t, err)
// Instantiate test data
event, objectIdentifier := NewOrganizationAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
ctx := context.WithValue(ctx, ContextKeyTopic, topicName)
assert.NoError(t, (*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
))
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentMessage(t, topicName, message, event, &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-legacy"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
topicName := "topic://audit-log/eu01/v1/resource-manager/folder-created"
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
// Instantiate audit api
auditApi, err := NewDynamicLegacyAuditApi(
messagingApi,
validator,
)
assert.NoError(t, err)
// Instantiate test data
event, objectIdentifier := NewFolderAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
ctx := context.WithValue(ctx, ContextKeyTopic, topicName)
assert.NoError(t, (*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
))
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentMessage(t, topicName, message, event, &traceParent, &traceState)
})
t.Run("Log private folder event", func(t *testing.T) {
defer solaceContainer.StopOnError()
// Create the queue and topic subscription in solace
queueName := "folder-event-private-legacy"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
topicName := "topic://audit-log/eu01/v1/resource-manager/folder-created"
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
// Instantiate audit api
auditApi, err := NewDynamicLegacyAuditApi(
messagingApi,
validator,
)
assert.NoError(t, err)
// Instantiate test data
event, objectIdentifier := NewFolderAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
ctx := context.WithValue(ctx, ContextKeyTopic, topicName)
assert.NoError(t, (*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
))
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentMessage(t, topicName, message, event, &traceParent, &traceState)
})
// Check logging of project events
t.Run("Log public project event", func(t *testing.T) {
defer solaceContainer.StopOnError()
// Create the queue and topic subscription in solace
queueName := "project-event-public-legacy"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
topicName := "topic://audit-log/eu01/v1/resource-manager/project-created"
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
// Instantiate audit api
auditApi, err := NewDynamicLegacyAuditApi(
messagingApi,
validator,
)
assert.NoError(t, err)
// Instantiate test data
event, objectIdentifier := NewProjectAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
ctx := context.WithValue(ctx, ContextKeyTopic, topicName)
assert.NoError(t, (*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
))
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentMessage(t, topicName, message, event, &traceParent, &traceState)
})
t.Run("Log private project event", func(t *testing.T) {
defer solaceContainer.StopOnError()
// Create the queue and topic subscription in solace
queueName := "project-event-private-legacy"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
topicName := "topic://audit-log/eu01/v1/resource-manager/project-created"
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
// Instantiate audit api
auditApi, err := NewDynamicLegacyAuditApi(
messagingApi,
validator,
)
assert.NoError(t, err)
// Instantiate test data
event, objectIdentifier := NewProjectAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
ctx := context.WithValue(ctx, ContextKeyTopic, topicName)
assert.NoError(t, (*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
))
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentMessage(t, topicName, message, event, &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, topicSubscriptionTopicPattern))
topicName := "topic://audit-log/eu01/v1/resource-manager/system-changed"
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
// Instantiate audit api
auditApi, err := NewDynamicLegacyAuditApi(
messagingApi,
validator,
)
assert.NoError(t, err)
// Instantiate test data
event := NewSystemAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
ctx := context.WithValue(ctx, ContextKeyTopic, topicName)
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, topicName, *message.Properties.To)
assert.Nil(t, message.ApplicationProperties["cloudEvents:traceparent"])
assert.Nil(t, message.ApplicationProperties["cloudEvents:tracestate"])
// Check deserialized message
var auditEvent LegacyAuditEvent
assert.NoError(t, json.Unmarshal(message.Data[0], &auditEvent))
assert.Equal(t, event.ProtoPayload.OperationName, auditEvent.EventName)
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(), auditEvent.EventTimeStamp)
assert.Equal(t, event.ProtoPayload.AuthenticationInfo.PrincipalId, auditEvent.Initiator.Id)
assert.Equal(t, "SYSTEM_EVENT", auditEvent.EventType)
assert.Equal(t, "INFO", auditEvent.Severity)
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Path, auditEvent.Request.Endpoint)
assert.Equal(t, event.ProtoPayload.RequestMetadata.CallerIp, auditEvent.SourceIpAddress)
assert.Equal(t, event.ProtoPayload.RequestMetadata.CallerSuppliedUserAgent, auditEvent.UserAgent)
})
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-legacy"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
topicName := "topic://audit-log/eu01/v1/resource-manager/organization-created"
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
// Instantiate audit api
auditApi, err := NewDynamicLegacyAuditApi(
messagingApi,
validator,
)
assert.NoError(t, err)
// Instantiate test data
event, objectIdentifier := NewOrganizationAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
ctx := context.WithValue(ctx, ContextKeyTopic, topicName)
assert.NoError(t, (*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
))
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentMessageWithDetails(t, topicName, message, event, &traceParent, &traceState)
})
}
func TestDynamicLegacyAuditApi_NewLegacyAuditApi_MessagingApiNil(t *testing.T) {
auditApi, err := NewDynamicLegacyAuditApi(nil, nil)
assert.Nil(t, auditApi)
assert.EqualError(t, err, "messaging api nil")
}
func TestDynamicLegacyAuditApi_ValidateAndSerialize_ValidationFailed(t *testing.T) {
expectedError := errors.New("expected error")
validator := &ProtobufValidatorMock{}
validator.On("Validate", mock.Anything).Return(expectedError)
var protobufValidator ProtobufValidator = validator
auditApi := DynamicLegacyAuditApi{validator: &protobufValidator}
event := NewSystemAuditEvent(nil)
_, err := auditApi.ValidateAndSerialize(event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
assert.ErrorIs(t, err, expectedError)
}
func TestDynamicLegacyAuditApi_Log_ValidationFailed(t *testing.T) {
expectedError := errors.New("expected error")
validator := &ProtobufValidatorMock{}
validator.On("Validate", mock.Anything).Return(expectedError)
var protobufValidator ProtobufValidator = validator
auditApi := DynamicLegacyAuditApi{validator: &protobufValidator}
event := NewSystemAuditEvent(nil)
err := auditApi.Log(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
assert.ErrorIs(t, err, expectedError)
}
func TestDynamicLegacyAuditApi_Log_NilEvent(t *testing.T) {
auditApi := DynamicLegacyAuditApi{}
err := auditApi.Log(context.Background(), nil, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
assert.ErrorIs(t, err, ErrEventNil)
}
func TestDynamicLegacyAuditApi_ConvertAndSerializeIntoLegacyFormatInvalidObjectIdentifierType(t *testing.T) {
customization := func(event *auditV1.AuditLogEntry,
objectIdentifier *auditV1.ObjectIdentifier) {
objectIdentifier.Type = "invalid"
}
event, objectIdentifier := NewProjectAuditEvent(&customization)
validator := &ProtobufValidatorMock{}
validator.On("Validate", mock.Anything).Return(nil)
var protobufValidator ProtobufValidator = validator
auditApi := DynamicLegacyAuditApi{validator: &protobufValidator}
_, err := auditApi.ValidateAndSerialize(event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier))
assert.ErrorIs(t, err, ErrUnsupportedRoutableType)
}

View file

@ -48,8 +48,6 @@ func TestLegacyAuditApi(t *testing.T) {
t.Run("Log public organization event", func(t *testing.T) { t.Run("Log public organization event", func(t *testing.T) {
defer solaceContainer.StopOnError() defer solaceContainer.StopOnError()
slog.Info("test abc")
// Create the queue and topic subscription in solace // Create the queue and topic subscription in solace
queueName := "org-event-public-legacy" queueName := "org-event-public-legacy"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
@ -548,17 +546,3 @@ func TestLegacyAuditApi_ConvertAndSerializeIntoLegacyFormatInvalidObjectIdentifi
_, err := auditApi.ValidateAndSerialize(event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier)) _, err := auditApi.ValidateAndSerialize(event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier))
assert.ErrorIs(t, err, ErrUnsupportedRoutableType) assert.ErrorIs(t, err, ErrUnsupportedRoutableType)
} }
func TestLegacyAuditApi_ConvertAndSerializeIntoLegacyFormat_NoObjectIdentifier(t *testing.T) {
event, _ := NewProjectAuditEvent(nil)
routableEvent := auditV1.RoutableAuditEvent{
OperationName: event.ProtoPayload.OperationName,
Visibility: auditV1.Visibility_VISIBILITY_PUBLIC,
ObjectIdentifier: nil,
Data: nil,
}
auditApi := LegacyAuditApi{}
_, err := auditApi.convertAndSerializeIntoLegacyFormat(event, &routableEvent)
assert.ErrorIs(t, err, ErrObjectIdentifierNil)
}

View file

@ -408,6 +408,7 @@ message AttributeContext {
// "email": "max@mail.schwarz", // "email": "max@mail.schwarz",
// "iss": "https://api.dev.stackit.cloud", // "iss": "https://api.dev.stackit.cloud",
// "jti": "45a196e0-480f-4c34-a592-dc5db81c8c3a" // "jti": "45a196e0-480f-4c34-a592-dc5db81c8c3a"
// "sub": "cd94f01a-df2e-4456-902f-48f5e57f0b63"
// } // }
// //
// Required: true // Required: true