Merged PR 666097: feat: Add implementation of core library

Related work items: #687250
This commit is contained in:
Christian Schaible 2024-10-30 10:32:07 +00:00
parent b7171c2177
commit 9337231a6f
53 changed files with 16485 additions and 0 deletions

View file

@ -0,0 +1,117 @@
pool:
vmImage: 'ubuntu-latest'
variables:
- name: bufVersion
value: v1.45.0
- name: golangCiLintVersion
value: v1.61.0
- name: goVersion
value: 1.23.2
- name: protobufValidateVersion
value: v1.1.0
- name: protobufVersion
value: v1.35.1
- name: GOPATH
value: '$(system.defaultWorkingDirectory)/gopath'
stages:
- stage: Build
jobs:
- job: GoBuildTest
displayName: Run build and tests
variables:
- name: isCiBuild
value: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')]
steps:
- task: GoTool@0
displayName: Install Go $(goVersion)
inputs:
version: $(goVersion)
- bash: |
set -e
go env -w GOMODCACHE="$(pwd)/.gomodcache"
displayName: Configure GOMODCACHE
- bash: |
set -e
go install google.golang.org/protobuf/cmd/protoc-gen-go@$(protobufVersion)
go install github.com/envoyproxy/protoc-gen-validate@$(protobufValidateVersion)
go install github.com/bufbuild/buf/cmd/buf@$(bufVersion)
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin $(golangCiLintVersion)
condition: succeeded()
displayName: Install build dependencies
- bash: |
set -e
echo on
go mod download
go mod tidy
go get ./...
condition: succeeded()
displayName: Download dependencies
- bash: |
set -e
echo on
rm -rf gen/
export PATH="$PATH:$GOPATH/bin"
buf format proto -w
cd proto
buf lint
buf generate
cd -
condition: succeeded()
displayName: Regenerate code from schema
- bash: |
set -e
echo on
export PATH="$PATH:$GOPATH/bin"
go fmt ./... && go vet ./... && golangci-lint run
condition: succeeded()
displayName: Format and lint
- bash: |
set -e
echo on
git diff HEAD --name-only --exit-code
condition: succeeded()
displayName: Check local changes after code generation and formatting
- bash: go build ./...
condition: succeeded()
displayName: Build
- bash: go test ./...
condition: succeeded()
displayName: Run tests
- task: SnykSecurityScan@1
condition: and(succeeded(), eq(variables.isCiBuild, true))
displayName: Snyk check (main branch)
inputs:
additionalArguments: "--remote-repo-url=$(Build.Repository.Uri)"
failOnIssues: false
monitorWhen: 'always'
organization: 'xx-sit-odj-stackit-public'
projectName: $(Build.Repository.Name)
serviceConnectionEndpoint: 'xx-sit-odj-stackit-public-snyk'
testType: 'app'
- task: SnykSecurityScan@1
condition: and(succeeded(), eq(variables.isCiBuild, false))
displayName: Snyk check
inputs:
additionalArguments: "--remote-repo-url=$(Build.Repository.Uri)"
failOnIssues: false
monitorWhen: 'never'
organization: 'xx-sit-odj-stackit-public'
projectName: $(Build.Repository.Name)
serviceConnectionEndpoint: 'xx-sit-odj-stackit-public-snyk'
testType: 'app'
- bash: sudo rm -rf .gomodcache
condition: always()
displayName: Clean up the local cache (.gomodcache)

View file

@ -0,0 +1,66 @@
trigger: none
pool:
vmImage: 'ubuntu-latest'
parameters:
- name: releaseType
displayName: Type of the release
type: string
default: minor
values:
- major
- minor
- patch
stages:
- stage: Release
variables:
- name: isMainBranch
value: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')]
jobs:
- job: Release
condition: eq(variables.isMainBranch, true)
displayName: Release
steps:
- checkout: self
persistCredentials: true
fetchDepth: 0
- bash: |
set -e
RELEASE_TYPE="${{ parameters.releaseType }}"
TAG_VERSION=$(git describe --tags --abbrev=0 --match v\*)
VERSION_NUMBER=$(echo ${TAG_VERSION} | sed 's/v//g' | sed 's/-.*//g')
MAJOR=$(echo ${VERSION_NUMBER} | cut -d. -f1)
MINOR=$(echo ${VERSION_NUMBER} | cut -d. -f2)
PATCH=$(echo ${VERSION_NUMBER} | cut -d. -f3)
echo "Current version ${VERSION_NUMBER}"
echo "Major version: ${MAJOR}"
echo "Minor version: ${MINOR}"
echo "Patch version: ${PATCH}"
if [ "${RELEASE_TYPE}" == "major" ]; then
RELEASE_VERSION=$((MAJOR + 1)).0.0
elif [ "${RELEASE_TYPE}" == "minor" ]; then
RELEASE_VERSION=${MAJOR}.$((MINOR + 1)).0
elif [ "${RELEASE_TYPE}" == "patch" ]; then
RELEASE_VERSION=${MAJOR}.${MINOR}.$((PATCH + 1))
else
echo "No release type specified"
exit 0
fi
RELEASE_VERSION="v${RELEASE_VERSION}"
echo "Release version: ${RELEASE_VERSION}"
COMMIT_ID=$(git rev-parse HEAD)
echo "Commit: ${COMMIT_ID}"
git tag ${RELEASE_VERSION} ${COMMIT_ID}
git push origin ${RELEASE_VERSION}
displayName: Release new ${{ parameters.releaseType }} version

4
.gitignore vendored
View file

@ -170,3 +170,7 @@ fabric.properties
# Editor-based Rest Client # Editor-based Rest Client
.idea/httpRequests .idea/httpRequests
# Buf
gen/java
gen/python

94
README.md Normal file
View file

@ -0,0 +1,94 @@
## audit-go
The audit-go library is the core library for validation and sending of audit events.
### API Documentation
The api documentation can be found
[here](https://developers.stackit.schwarz/domains/core-platform/audit-log/sdk/overview/).
### Supported data types for routing
The following data types are currently supported for routing.
| ObjectType | Routable to customer | Description |
|--------------|----------------------|----------------------|
| system | no | The STACKIT system |
| project | yes | STACKIT project |
| organization | yes | STACKIT organization |
| folder | yes | STACKIT folder |
### Additional API implementations
There's already an implementation draft of the api for the new dynamically routing
audit log solution. As the implementation of the system has not officially been
started yet, it's only a draft with integration tests.
The API code is private to not confuse users or loose data until the new system
is ready to be used.
The code can be found in the [api_routable.go](./api_routable.go) and
[api_routable_test.go](./api_routable_test.go) files.
### Development
#### Linter
The linter *golangci-lint* can either be installed via package manager (e.g. brew) or
by running the following command in the terminal:
```shell
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.61.0
```
#### Schema Generation
Go structs are generated from Protobuf schema by using [Buf](https://buf.build) and some plugins.
The buf plugins are referenced in the *proto/buf.gen.yaml* file and are expected
to be installed locally.
The schema generator also generates code to validate constraints specified
in the schema.
Buf and the required plugins can either be installed via package manager (e.g. brew)
or manually by running:
```shell
go install github.com/bufbuild/buf/cmd/buf@v1.45.0 #Pipeline: bufVersion
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.35.1 #Pipeline: protobufVersion, go.mod: buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go
go install github.com/envoyproxy/protoc-gen-validate@v1.1.0 #Pipeline: protobufValidateVersion, go.mod: google.golang.org/protobuf
```
Please check that the versions above match the versions in the *go.mod* file
and the *.azuredevops/build-pipeline.yml* file.
Then the schema can be generated:
```bash
cd proto
buf generate
```
#### Build
The library can be built by executing the following commands:
```bash
go mod download && go mod tidy && go get ./... && go fmt ./... && go vet ./... && golangci-lint run && go build ./... && go test ./...
```
##### Testcontainers
To run the tests **Docker** is needed as [Testcontainers](https://testcontainers.com/)
is used to run integration tests using a solace docker container.
#### Register buf validation schema in IntelliJ / Goland
The schema files use `Buf` protobuf extensions for validation of constraints.
To register the schema in IntelliJ / Goland clone the repo and add the import path:
```bash
git clone https://github.com/bufbuild/protovalidate.git
```
IntelliJ/Goland > Settings > Languages & Frameworks > Protocol Buffers > Import Paths > +
(Add Path) > …/protovalidate/proto/protovalidate

15
audit-go.iml Normal file
View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="FacetManager">
<facet type="Python" name="Python">
<configuration sdkName="Python 3.9 (kafka)" />
</facet>
</component>
<component name="Go" enabled="true" />
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Python 3.9 (kafka) interpreter library" level="application" />
</component>
</module>

292
audit/api/api.go Normal file
View file

@ -0,0 +1,292 @@
package api
import (
"context"
"time"
"github.com/google/uuid"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"google.golang.org/protobuf/proto"
)
type EventType string
const (
EventTypeAdminActivity EventType = "admin-activity"
EventTypeSystemEvent EventType = "system-event"
EventTypePolicyDenied EventType = "policy-denied"
EventTypeDataAccess EventType = "data-access"
)
type ObjectType string
const (
ObjectTypeSystem ObjectType = "system"
ObjectTypeOrganization ObjectType = "organization"
ObjectTypeFolder ObjectType = "folder"
ObjectTypeProject ObjectType = "project"
)
func ObjectTypeFromPluralString(value string) ObjectType {
pluralSuffix := "s"
switch value {
case string(ObjectTypeOrganization) + pluralSuffix:
return ObjectTypeOrganization
case string(ObjectTypeFolder) + pluralSuffix:
return ObjectTypeFolder
case string(ObjectTypeProject) + pluralSuffix:
return ObjectTypeProject
case string(ObjectTypeSystem):
return ObjectTypeSystem
default:
return ObjectType(value)
}
}
func (t ObjectType) IsSupportedType() error {
switch t {
case ObjectTypeOrganization:
fallthrough
case ObjectTypeFolder:
fallthrough
case ObjectTypeProject:
fallthrough
case ObjectTypeSystem:
return nil
default:
return ErrUnknownObjectType
}
}
func (t ObjectType) Plural() string {
pluralSuffix := "s"
switch t {
case ObjectTypeOrganization:
return string(ObjectTypeOrganization) + pluralSuffix
case ObjectTypeFolder:
return string(ObjectTypeFolder) + pluralSuffix
case ObjectTypeProject:
return string(ObjectTypeProject) + pluralSuffix
case ObjectTypeSystem:
return string(ObjectTypeSystem)
default:
return ""
}
}
var SystemIdentifier = &auditV1.ObjectIdentifier{Identifier: uuid.Nil.String(), Type: string(ObjectTypeSystem)}
var RoutableSystemIdentifier = NewRoutableIdentifier(SystemIdentifier)
// AuditApi is the interface to log audit events.
//
// It provides a Log method that can be used to validate and directly send events.
// If the transactional outbox pattern should be used, the ValidateAndSerialize and Send methods
// can be called manually to decouple operations.
type AuditApi interface {
// Log is a convenience method that validates, serializes and sends data over the wire.
// If the transactional outbox pattern should be used, the ValidateAndSerialize method
// and Send method can be called separately.
// If an error is returned it is the responsibility of the caller to retry. The api does
// not store, buffer events or retry failed invocation automatically.
//
// Parameters:
// * ctx - the context object
// * event - the auditV1.AuditEvent
// * visibility - route the event only internally or to the customer (no routing in the legacy solution)
// * routableIdentifier - the identifier of the object
//
// Returns:
// * an error if the validation, serialization or send failed
Log(
ctx context.Context,
event *auditV1.AuditLogEntry,
visibility auditV1.Visibility,
routableIdentifier *RoutableIdentifier,
) error
// LogWithTrace is a convenience method that validates, serializes and sends data over the wire.
// If the transactional outbox pattern should be used, the ValidateAndSerializeWithTrace method
// and Send method can be called separately. The method accepts traceParent and traceState
// parameters to put into attributes of the AuditLogEntry.
// If an error is returned it is the responsibility of the caller to retry. The api does not store,
// buffer events or retry failed invocation automatically.
//
// Parameters:
// * ctx - the context object
// * event - the auditV1.AuditEvent
// * visibility - route the event only internally or to the customer (no routing in the legacy solution)
// * routableIdentifier - the identifier of the object
// * traceParent - optional trace parent
// * traceState - optional trace state
//
// Returns:
// * an error if the validation, serialization or send failed
LogWithTrace(
ctx context.Context,
event *auditV1.AuditLogEntry,
visibility auditV1.Visibility,
routableIdentifier *RoutableIdentifier,
traceParent *string,
traceState *string,
) error
// ValidateAndSerialize validates and serializes the event into a byte representation.
// The result has to be sent explicitly by calling the Send method.
//
// Parameters:
// * event - the auditV1.AuditEvent
// * visibility - route the event only internally or to the customer (no routing in the legacy solution)
// * routableIdentifier - the identifier of the object
//
// Returns:
// * the CloudEvent (i.e. the serialized AuditLogEntry with metadata)
// * an error if validation or serialization failed
ValidateAndSerialize(
event *auditV1.AuditLogEntry,
visibility auditV1.Visibility,
routableIdentifier *RoutableIdentifier,
) (*CloudEvent, error)
// ValidateAndSerializeWithTrace validates and serializes the event into a byte representation.
// The result has to be sent explicitly by calling the Send method.
//
// Parameters:
// * event - the auditV1.AuditEvent
// * visibility - route the event only internally or to the customer (no routing in the legacy solution)
// * routableIdentifier - the identifier of the object
// * traceParent - optional trace parent
// * traceState - optional trace state
//
// Returns:
// * the CloudEvent (i.e. the serialized AuditLogEntry with metadata)
// * an error if validation or serialization failed
ValidateAndSerializeWithTrace(
event *auditV1.AuditLogEntry,
visibility auditV1.Visibility,
routableIdentifier *RoutableIdentifier,
traceParent *string,
traceState *string,
) (*CloudEvent, error)
// Send the serialized content as byte array to the audit log system.
// If an error is returned it is the responsibility of the caller to
// retry. The api does not store, buffer events or retry failed
// invocation automatically.
//
// Parameters:
// * ctx - the context object
// * routableIdentifier - the identifier of the object
// * cloudEvent - the serialized AuditLogEntry with metadata
//
// Returns:
// * an error if the event could not be sent
Send(
ctx context.Context,
routableIdentifier *RoutableIdentifier,
cloudEvent *CloudEvent,
) error
}
// ProtobufValidator is an abstraction for validators.
// Concrete implementations are e.g. protovalidate.Validator
type ProtobufValidator interface {
Validate(msg proto.Message) error
}
// CloudEvent is a representation of a cloudevents.io object.
//
// More information about the schema and attribute semantics can be found here:
// https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#required-attributes
type CloudEvent struct {
// The version of the CloudEvents specification which the event uses.
// This enables the interpretation of the context. Compliant event producers MUST use a value of 1.0
// when referring to this version of the specification.
//
// Currently, this attribute will only have the 'major' and 'minor' version numbers included in it.
// This allows for 'patch' changes to the specification to be made without changing this property's
// value in the serialization.
SpecVersion string
// The source system uri-reference. Producers MUST ensure that source + id is unique for each distinct event.
Source string
// Identifier of the event. Producers MUST ensure that source + id is unique for each distinct event.
// If a duplicate event is re-sent (e.g. due to a network error) it MAY have the same id.
// Consumers MAY assume that Events with identical source and id are duplicates.
Id string
// The time when the event happened
Time time.Time
// The content type of the payload
// Examples could be:
// - application/cloudevents+json
// - application/cloudevents+json; charset=UTF-8
// - application/cloudevents-batch+json
// - application/cloudevents+protobuf
// - application/cloudevents+avro
// Source: https://github.com/cloudevents/spec/blob/main/cloudevents/formats/protobuf-format.md
DataContentType string
// The object type (i.e. the fully qualified protobuf type name)
DataType string
// The identifier of the referring object.
Subject string
// The serialized payload
Data []byte
// Optional W3C conform trace parent:
// https://www.w3.org/TR/trace-context/#traceparent-header
//
// Format: <version>-<trace-id>-<parent-id>-<trace-flags>
//
// Examples:
// "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
TraceParent *string
// Optional W3C conform trace state header:
// https://www.w3.org/TR/trace-context/#tracestate-header
//
// Format: <key1>=<value1>[,<keyN>=<valueN>]
//
// Examples:
// "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"
TraceState *string
}
// 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 object identifier
Resolve(routableIdentifier *RoutableIdentifier) (string, error)
}
type RoutableIdentifier struct {
Identifier string
Type ObjectType
}
func NewRoutableIdentifier(objectIdentifier *auditV1.ObjectIdentifier) *RoutableIdentifier {
if objectIdentifier == nil {
return nil
}
return &RoutableIdentifier{
Identifier: objectIdentifier.Identifier,
Type: ObjectType(objectIdentifier.Type),
}
}
func (r RoutableIdentifier) ToObjectIdentifier() *auditV1.ObjectIdentifier {
return &auditV1.ObjectIdentifier{
Identifier: r.Identifier,
Type: string(r.Type),
}
}

278
audit/api/api_common.go Normal file
View file

@ -0,0 +1,278 @@
package api
import (
"context"
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/telemetry"
"errors"
"fmt"
"strings"
"github.com/google/uuid"
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/messaging"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"google.golang.org/protobuf/proto"
)
// ContentTypeCloudEventsProtobuf the cloudevents protobuf content-type sent in metadata of messages
const ContentTypeCloudEventsProtobuf = "application/cloudevents+protobuf"
const ContentTypeCloudEventsJson = "application/cloudevents+json; charset=UTF-8"
// ErrAttributeIdentifierInvalid indicates that the object identifier
// and the identifier in the checked attribute do not match
var ErrAttributeIdentifierInvalid = errors.New("attribute identifier invalid")
// ErrAttributeTypeInvalid indicates that an invalid type has been provided.
var ErrAttributeTypeInvalid = errors.New("attribute type invalid")
// ErrCloudEventNil states that the given cloud event is nil
var ErrCloudEventNil = errors.New("cloud event nil")
// ErrEventNil indicates that the event was nil
var ErrEventNil = errors.New("event is nil")
// ErrInvalidRoutableIdentifierForSystemEvent states that the routable identifier is not valid for a system event
var ErrInvalidRoutableIdentifierForSystemEvent = errors.New("invalid identifier for system event")
// ErrMessagingApiNil states that the messaging api is nil
var ErrMessagingApiNil = errors.New("messaging api nil")
// ErrObjectIdentifierNil indicates that the object identifier was nil
var ErrObjectIdentifierNil = errors.New("object identifier is nil")
// ErrObjectIdentifierVisibilityMismatch indicates that a reference mismatch was detected.
//
// Valid combinations are:
// * Visibility: Public, ObjectIdentifier: <type>
// * Visibility: Private, ObjectIdentifier: <type | system>
var ErrObjectIdentifierVisibilityMismatch = errors.New("object reference visibility mismatch")
// ErrTopicNameResolverNil states that the topic name resolve is nil
var ErrTopicNameResolverNil = errors.New("topic name resolver nil")
// ErrUnknownObjectType indicates that the given input is an unknown object type
var ErrUnknownObjectType = errors.New("unknown object type")
// ErrUnsupportedEventTypeDataAccess states that the event type "data-access" is currently not supported
var ErrUnsupportedEventTypeDataAccess = errors.New("unsupported event type data access")
// ErrUnsupportedObjectIdentifierType indicates that an unsupported object identifier type has been provided
var ErrUnsupportedObjectIdentifierType = errors.New("unsupported object identifier type")
// ErrUnsupportedRoutableType indicates that the given input is an unsupported routable type
var ErrUnsupportedRoutableType = errors.New("unsupported routable type")
func validateAndSerializePartially(
validator *ProtobufValidator,
event *auditV1.AuditLogEntry,
visibility auditV1.Visibility,
routableIdentifier *RoutableIdentifier,
) (*auditV1.RoutableAuditEvent, error) {
// Check preconditions
err := validateAuditLogEntry(validator, event, visibility, routableIdentifier)
if err != nil {
return nil, err
}
// Serialize the AuditLogEntry and wrap it into a RoutableAuditEvent
routableEvent, err := newValidatedRoutableAuditEvent(validator, event, visibility, routableIdentifier)
if err != nil {
return nil, err
}
return routableEvent, nil
}
func newValidatedRoutableAuditEvent(
validator *ProtobufValidator,
event *auditV1.AuditLogEntry,
visibility auditV1.Visibility,
routableIdentifier *RoutableIdentifier) (*auditV1.RoutableAuditEvent, error) {
// Test serialization even if the data is dropped later when logging to the legacy solution
auditEventBytes, err := proto.Marshal(event)
if err != nil {
return nil, err
}
payload := auditV1.UnencryptedData{
Data: auditEventBytes,
ProtobufType: fmt.Sprintf("%v", event.ProtoReflect().Descriptor().FullName()),
}
routableEvent := auditV1.RoutableAuditEvent{
OperationName: event.ProtoPayload.OperationName,
ObjectIdentifier: routableIdentifier.ToObjectIdentifier(),
Visibility: visibility,
Data: &auditV1.RoutableAuditEvent_UnencryptedData{UnencryptedData: &payload},
}
err = (*validator).Validate(&routableEvent)
if err != nil {
return nil, err
}
return &routableEvent, nil
}
func validateAuditLogEntry(
validator *ProtobufValidator,
event *auditV1.AuditLogEntry,
visibility auditV1.Visibility,
routableIdentifier *RoutableIdentifier,
) error {
// Return error if the given event or object identifier is nil
if event == nil {
return ErrEventNil
}
if routableIdentifier == nil {
return ErrObjectIdentifierNil
}
// Validate the actual event
err := (*validator).Validate(event)
if err != nil {
return err
}
// Ensure that a valid object identifier is set if the event is public
if isSystemIdentifier(routableIdentifier) && visibility == auditV1.Visibility_VISIBILITY_PUBLIC {
return ErrObjectIdentifierVisibilityMismatch
}
// Check that provided identifier type is supported
if err := routableIdentifier.Type.IsSupportedType(); err != nil {
if errors.Is(err, ErrUnknownObjectType) {
return ErrUnsupportedRoutableType
}
return err
}
// Check identifier consistency across event attributes
if strings.HasSuffix(event.LogName, string(EventTypeSystemEvent)) {
if !(routableIdentifier.Identifier == SystemIdentifier.Identifier && routableIdentifier.Type == ObjectTypeSystem) {
return ErrInvalidRoutableIdentifierForSystemEvent
}
// The resource name can either contain the system identifier or another resource identifier
} else {
if err := areIdentifiersIdentical(routableIdentifier, event.LogName); err != nil {
return err
}
if err := areIdentifiersIdentical(routableIdentifier, event.ProtoPayload.ResourceName); err != nil {
return err
}
}
return nil
}
// Send implements AuditApi.Send
func send(
topicNameResolver *TopicNameResolver,
messagingApi *messaging.Api,
ctx context.Context,
routableIdentifier *RoutableIdentifier,
cloudEvent *CloudEvent,
) error {
// Check that given objects are not nil
if topicNameResolver == nil {
return ErrTopicNameResolverNil
}
if messagingApi == nil {
return ErrMessagingApiNil
}
if cloudEvent == nil {
return ErrCloudEventNil
}
if routableIdentifier == nil {
return ErrObjectIdentifierNil
}
// Check that provided identifier type is supported
if err := routableIdentifier.Type.IsSupportedType(); err != nil {
if errors.Is(err, ErrUnknownObjectType) {
return ErrUnsupportedRoutableType
}
return err
}
topic, err := (*topicNameResolver).Resolve(routableIdentifier)
if err != nil {
return err
}
// Naming according to AMQP protocol binding spec
// https://github.com/cloudevents/spec/blob/main/cloudevents/bindings/amqp-protocol-binding.md
applicationAttributes := make(map[string]any)
applicationAttributes["cloudEvents:specversion"] = cloudEvent.SpecVersion
applicationAttributes["cloudEvents:source"] = cloudEvent.Source
applicationAttributes["cloudEvents:id"] = cloudEvent.Id
applicationAttributes["cloudEvents:time"] = cloudEvent.Time.UnixMilli()
applicationAttributes["cloudEvents:datacontenttype"] = cloudEvent.DataContentType
applicationAttributes["cloudEvents:type"] = cloudEvent.DataType
applicationAttributes["cloudEvents:subject"] = cloudEvent.Subject
if cloudEvent.TraceParent != nil {
applicationAttributes["cloudEvents:traceparent"] = *cloudEvent.TraceParent
}
if cloudEvent.TraceState != nil {
applicationAttributes["cloudEvents:tracestate"] = *cloudEvent.TraceState
}
// Telemetry
applicationAttributes["cloudEvents:sdklanguage"] = "go"
auditGoVersion := telemetry.AuditGoVersion
if auditGoVersion != "" {
applicationAttributes["cloudEvents:sdkversion"] = auditGoVersion
}
auditGoGrpcVersion := telemetry.AuditGoGrpcVersion
if auditGoGrpcVersion != "" {
applicationAttributes["cloudEvents:sdkgrpcversion"] = auditGoGrpcVersion
}
auditGoHttpVersion := telemetry.AuditGoHttpVersion
if auditGoHttpVersion != "" {
applicationAttributes["cloudEvents:sdkhttpversion"] = auditGoHttpVersion
}
return (*messagingApi).Send(
ctx,
topic,
(*cloudEvent).Data,
(*cloudEvent).DataContentType,
applicationAttributes)
}
func isSystemIdentifier(identifier *RoutableIdentifier) bool {
if identifier.Identifier == uuid.Nil.String() && identifier.Type == ObjectTypeSystem {
return true
}
return false
}
func areIdentifiersIdentical(routableIdentifier *RoutableIdentifier, logName string) error {
dataType, identifier := getTypeAndIdentifierFromString(logName)
objectType := ObjectTypeFromPluralString(dataType)
err := objectType.IsSupportedType()
if err != nil {
return err
}
return areTypeAndIdentifierIdentical(routableIdentifier, objectType, identifier)
}
func areTypeAndIdentifierIdentical(routableIdentifier *RoutableIdentifier, dataType ObjectType, identifier string) error {
if routableIdentifier.Identifier != identifier {
return ErrAttributeIdentifierInvalid
}
if routableIdentifier.Type != dataType {
return ErrAttributeTypeInvalid
}
return nil
}
func getTypeAndIdentifierFromString(input string) (string, string) {
parts := strings.Split(input, "/")
dataType := parts[0]
identifier := parts[1]
return dataType, identifier
}

View file

@ -0,0 +1,413 @@
package api
import (
"context"
"errors"
"fmt"
"strings"
"testing"
"time"
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/messaging"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"github.com/bufbuild/protovalidate-go"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"google.golang.org/protobuf/proto"
)
type MessagingApiMock struct {
mock.Mock
}
func (m *MessagingApiMock) Send(
ctx context.Context,
topic string,
data []byte,
contentType string,
applicationProperties map[string]any,
) error {
args := m.Called(ctx, topic, data, contentType, applicationProperties)
return args.Error(0)
}
type ProtobufValidatorMock struct {
mock.Mock
}
func (m *ProtobufValidatorMock) Validate(msg proto.Message) error {
args := m.Called(msg)
return args.Error(0)
}
type TopicNameResolverMock struct {
mock.Mock
}
func (m *TopicNameResolverMock) Resolve(routableIdentifier *RoutableIdentifier) (string, error) {
args := m.Called(routableIdentifier)
return args.String(0), args.Error(1)
}
func NewValidator(t *testing.T) ProtobufValidator {
validator, err := protovalidate.New()
var protoValidator ProtobufValidator = validator
assert.NoError(t, err)
return protoValidator
}
func Test_ValidateAndSerializePartially_EventNil(t *testing.T) {
validator := NewValidator(t)
_, err := validateAndSerializePartially(
&validator, nil, auditV1.Visibility_VISIBILITY_PUBLIC, nil)
assert.ErrorIs(t, err, ErrEventNil)
}
func Test_ValidateAndSerializePartially_AuditEventValidationFailed(t *testing.T) {
validator := NewValidator(t)
event, objectIdentifier := newOrganizationAuditEvent(nil)
event.LogName = ""
_, err := validateAndSerializePartially(
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier))
assert.EqualError(t, err, "validation error:\n - log_name: value is required [required]")
}
func Test_ValidateAndSerializePartially_RoutableEventValidationFailed(t *testing.T) {
validator := NewValidator(t)
event, objectIdentifier := newOrganizationAuditEvent(nil)
_, err := validateAndSerializePartially(&validator, event, 3, NewRoutableIdentifier(objectIdentifier))
assert.EqualError(t, err, "validation error:\n - visibility: value must be one of the defined enum values [enum.defined_only]")
}
func Test_ValidateAndSerializePartially_CheckVisibility_Event(t *testing.T) {
validator := NewValidator(t)
event, objectIdentifier := newOrganizationAuditEvent(nil)
t.Run("Visibility public - object identifier nil", func(t *testing.T) {
_, err := validateAndSerializePartially(
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, nil)
assert.ErrorIs(t, err, ErrObjectIdentifierNil)
})
t.Run("Visibility private - object identifier nil", func(t *testing.T) {
_, err := validateAndSerializePartially(
&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, nil)
assert.ErrorIs(t, err, ErrObjectIdentifierNil)
})
t.Run("Visibility public - object identifier system", func(t *testing.T) {
_, err := validateAndSerializePartially(
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
assert.ErrorIs(t, err, ErrObjectIdentifierVisibilityMismatch)
})
t.Run("Visibility public - object identifier set", func(t *testing.T) {
routableEvent, err := validateAndSerializePartially(
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier))
assert.NoError(t, err)
assert.NotNil(t, routableEvent)
})
t.Run("Visibility private - object identifier system", func(t *testing.T) {
_, err := validateAndSerializePartially(
&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, RoutableSystemIdentifier)
assert.ErrorIs(t, err, ErrAttributeIdentifierInvalid)
})
t.Run("Visibility private - object identifier set", func(t *testing.T) {
routableEvent, err := validateAndSerializePartially(
&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, NewRoutableIdentifier(objectIdentifier))
assert.NoError(t, err)
assert.NotNil(t, routableEvent)
})
}
func Test_ValidateAndSerializePartially_CheckVisibility_SystemEvent(t *testing.T) {
validator := NewValidator(t)
event := newSystemAuditEvent(nil)
t.Run("Visibility public - object identifier nil", func(t *testing.T) {
_, err := validateAndSerializePartially(
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, nil)
assert.ErrorIs(t, err, ErrObjectIdentifierNil)
})
t.Run("Visibility private - object identifier nil", func(t *testing.T) {
_, err := validateAndSerializePartially(
&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, nil)
assert.ErrorIs(t, err, ErrObjectIdentifierNil)
})
t.Run("Visibility public - object identifier system", func(t *testing.T) {
_, err := validateAndSerializePartially(
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
assert.ErrorIs(t, err, ErrObjectIdentifierVisibilityMismatch)
})
t.Run("Visibility public - object identifier set", func(t *testing.T) {
_, err := validateAndSerializePartially(
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(
&auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: string(ObjectTypeOrganization)}))
assert.ErrorIs(t, err, ErrInvalidRoutableIdentifierForSystemEvent)
})
t.Run("Visibility private - object identifier system", func(t *testing.T) {
routableEvent, err := validateAndSerializePartially(
&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, RoutableSystemIdentifier)
assert.NoError(t, err)
assert.NotNil(t, routableEvent)
})
t.Run("Visibility private - object identifier set", func(t *testing.T) {
_, err := validateAndSerializePartially(
&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, NewRoutableIdentifier(
&auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: string(ObjectTypeOrganization)}))
assert.ErrorIs(t, err, ErrInvalidRoutableIdentifierForSystemEvent)
})
}
func Test_ValidateAndSerializePartially_UnsupportedIdentifierType(t *testing.T) {
validator := NewValidator(t)
event, objectIdentifier := newFolderAuditEvent(nil)
objectIdentifier.Type = "invalid"
_, err := validateAndSerializePartially(
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier))
assert.ErrorIs(t, err, ErrUnsupportedRoutableType)
}
func Test_ValidateAndSerializePartially_LogNameIdentifierMismatch(t *testing.T) {
validator := NewValidator(t)
event, objectIdentifier := newFolderAuditEvent(nil)
parts := strings.Split(event.LogName, "/")
identifier := parts[1]
t.Run("LogName type mismatch", func(t *testing.T) {
event.LogName = fmt.Sprintf("projects/%s/logs/admin-activity", identifier)
_, err := validateAndSerializePartially(
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier))
assert.ErrorIs(t, err, ErrAttributeTypeInvalid)
})
t.Run("LogName identifier mismatch", func(t *testing.T) {
event.LogName = fmt.Sprintf("folders/%s/logs/admin-activity", uuid.NewString())
_, err := validateAndSerializePartially(
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier))
assert.ErrorIs(t, err, ErrAttributeIdentifierInvalid)
})
}
func Test_ValidateAndSerializePartially_ResourceNameIdentifierMismatch(t *testing.T) {
validator := NewValidator(t)
event, objectIdentifier := newFolderAuditEvent(nil)
parts := strings.Split(event.ProtoPayload.ResourceName, "/")
identifier := parts[1]
t.Run("ResourceName type mismatch", func(t *testing.T) {
event.ProtoPayload.ResourceName = fmt.Sprintf("projects/%s", identifier)
_, err := validateAndSerializePartially(
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier))
assert.ErrorIs(t, err, ErrAttributeTypeInvalid)
})
t.Run("ResourceName identifier mismatch", func(t *testing.T) {
event.ProtoPayload.ResourceName = fmt.Sprintf("folders/%s", uuid.NewString())
_, err := validateAndSerializePartially(
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier))
assert.ErrorIs(t, err, ErrAttributeIdentifierInvalid)
})
}
func Test_ValidateAndSerializePartially_SystemEvent(t *testing.T) {
validator := NewValidator(t)
event := newSystemAuditEvent(nil)
routableEvent, err := validateAndSerializePartially(
&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, RoutableSystemIdentifier)
assert.NoError(t, err)
assert.Equal(t, event.LogName, fmt.Sprintf("system/%s/logs/%s", SystemIdentifier.Identifier, EventTypeSystemEvent))
assert.True(t, proto.Equal(routableEvent.ObjectIdentifier, SystemIdentifier))
}
func Test_Send_TopicNameResolverNil(t *testing.T) {
err := send(nil, nil, context.Background(), nil, nil)
assert.ErrorIs(t, err, ErrTopicNameResolverNil)
}
func Test_Send_TopicNameResolutionError(t *testing.T) {
expectedError := errors.New("expected error")
topicNameResolverMock := TopicNameResolverMock{}
topicNameResolverMock.On("Resolve", mock.Anything).Return("topic", expectedError)
var topicNameResolver TopicNameResolver = &topicNameResolverMock
var cloudEvent = CloudEvent{}
var messagingApi messaging.Api = &messaging.AmqpApi{}
err := send(&topicNameResolver, &messagingApi, context.Background(), RoutableSystemIdentifier, &cloudEvent)
assert.ErrorIs(t, err, expectedError)
}
func Test_Send_MessagingApiNil(t *testing.T) {
var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: "test"}
err := send(&topicNameResolver, nil, context.Background(), nil, nil)
assert.ErrorIs(t, err, ErrMessagingApiNil)
}
func Test_Send_CloudEventNil(t *testing.T) {
var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: "test"}
var messagingApi messaging.Api = &messaging.AmqpApi{}
err := send(&topicNameResolver, &messagingApi, context.Background(), nil, nil)
assert.ErrorIs(t, err, ErrCloudEventNil)
}
func Test_Send_ObjectIdentifierNil(t *testing.T) {
var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: "test"}
var messagingApi messaging.Api = &messaging.AmqpApi{}
var cloudEvent = CloudEvent{}
err := send(&topicNameResolver, &messagingApi, context.Background(), nil, &cloudEvent)
assert.ErrorIs(t, err, ErrObjectIdentifierNil)
}
func Test_Send_UnsupportedObjectIdentifierType(t *testing.T) {
var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: "test"}
var messagingApi messaging.Api = &messaging.AmqpApi{}
var cloudEvent = CloudEvent{}
var objectIdentifier = auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: "unsupported"}
err := send(&topicNameResolver, &messagingApi, context.Background(), NewRoutableIdentifier(&objectIdentifier), &cloudEvent)
assert.ErrorIs(t, err, ErrUnsupportedRoutableType)
}
func Test_Send(t *testing.T) {
topicNameResolverMock := TopicNameResolverMock{}
topicNameResolverMock.On("Resolve", mock.Anything).Return("topic", nil)
var topicNameResolver TopicNameResolver = &topicNameResolverMock
messagingApiMock := MessagingApiMock{}
messagingApiMock.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
var messagingApi messaging.Api = &messagingApiMock
var cloudEvent = CloudEvent{}
assert.NoError(t, send(&topicNameResolver, &messagingApi, context.Background(), RoutableSystemIdentifier, &cloudEvent))
assert.True(t, messagingApiMock.AssertNumberOfCalls(t, "Send", 1))
}
func Test_SendAllHeadersSet(t *testing.T) {
topicNameResolverMock := TopicNameResolverMock{}
topicNameResolverMock.On("Resolve", mock.Anything).Return("topic", nil)
var topicNameResolver TopicNameResolver = &topicNameResolverMock
messagingApiMock := MessagingApiMock{}
messagingApiMock.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
var messagingApi messaging.Api = &messagingApiMock
traceState := "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"
traceParent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
expectedTime := time.Now()
var cloudEvent = CloudEvent{
SpecVersion: "1.0",
Source: "resourcemanager",
Id: "id",
Time: expectedTime,
DataContentType: ContentTypeCloudEventsProtobuf,
DataType: "type",
Subject: "subject",
TraceParent: &traceParent,
TraceState: &traceState,
}
assert.NoError(t, send(&topicNameResolver, &messagingApi, context.Background(), RoutableSystemIdentifier, &cloudEvent))
assert.True(t, messagingApiMock.AssertNumberOfCalls(t, "Send", 1))
arguments := messagingApiMock.Calls[0].Arguments
topic := arguments.Get(1).(string)
assert.Equal(t, "topic", topic)
contentType := arguments.Get(3).(string)
assert.Equal(t, ContentTypeCloudEventsProtobuf, contentType)
applicationProperties := arguments.Get(4).(map[string]any)
assert.Equal(t, "1.0", applicationProperties["cloudEvents:specversion"])
assert.Equal(t, "resourcemanager", applicationProperties["cloudEvents:source"])
assert.Equal(t, "id", applicationProperties["cloudEvents:id"])
assert.Equal(t, expectedTime.UnixMilli(), applicationProperties["cloudEvents:time"])
assert.Equal(t, ContentTypeCloudEventsProtobuf, applicationProperties["cloudEvents:datacontenttype"])
assert.Equal(t, "type", applicationProperties["cloudEvents:type"])
assert.Equal(t, "subject", applicationProperties["cloudEvents:subject"])
assert.Equal(t, traceParent, applicationProperties["cloudEvents:traceparent"])
assert.Equal(t, traceState, applicationProperties["cloudEvents:tracestate"])
messagingApiMock.AssertExpectations(t)
}
func Test_SendWithoutOptionalHeadersSet(t *testing.T) {
topicNameResolverMock := TopicNameResolverMock{}
topicNameResolverMock.On("Resolve", mock.Anything).Return("topic", nil)
var topicNameResolver TopicNameResolver = &topicNameResolverMock
messagingApiMock := MessagingApiMock{}
messagingApiMock.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
var messagingApi messaging.Api = &messagingApiMock
expectedTime := time.Now()
var cloudEvent = CloudEvent{
SpecVersion: "1.0",
Source: "resourcemanager",
Id: "id",
Time: expectedTime,
DataContentType: ContentTypeCloudEventsProtobuf,
DataType: "type",
Subject: "subject",
}
assert.NoError(t, send(&topicNameResolver, &messagingApi, context.Background(), RoutableSystemIdentifier, &cloudEvent))
assert.True(t, messagingApiMock.AssertNumberOfCalls(t, "Send", 1))
arguments := messagingApiMock.Calls[0].Arguments
topic := arguments.Get(1).(string)
assert.Equal(t, "topic", topic)
contentType := arguments.Get(3).(string)
assert.Equal(t, ContentTypeCloudEventsProtobuf, contentType)
applicationProperties := arguments.Get(4).(map[string]any)
assert.Equal(t, "1.0", applicationProperties["cloudEvents:specversion"])
assert.Equal(t, "resourcemanager", applicationProperties["cloudEvents:source"])
assert.Equal(t, "id", applicationProperties["cloudEvents:id"])
assert.Equal(t, expectedTime.UnixMilli(), applicationProperties["cloudEvents:time"])
assert.Equal(t, ContentTypeCloudEventsProtobuf, applicationProperties["cloudEvents:datacontenttype"])
assert.Equal(t, "type", applicationProperties["cloudEvents:type"])
assert.Equal(t, "subject", applicationProperties["cloudEvents:subject"])
assert.Equal(t, nil, applicationProperties["cloudEvents:traceparent"])
assert.Equal(t, nil, applicationProperties["cloudEvents:tracestate"])
messagingApiMock.AssertExpectations(t)
}

164
audit/api/api_legacy.go Normal file
View file

@ -0,0 +1,164 @@
package api
import (
"context"
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/messaging"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"errors"
"strings"
"google.golang.org/protobuf/proto"
)
const DataTypeLegacyAuditEventV1 = "audit.v1.LegacyAuditEvent"
// LegacyTopicNameResolver implements TopicNameResolver.
// A hard-coded topic name is used, routing identifiers are ignored.
type LegacyTopicNameResolver struct {
topicName string
}
// Resolve implements TopicNameResolver.Resolve
func (r *LegacyTopicNameResolver) Resolve(*RoutableIdentifier) (string, error) {
return r.topicName, nil
}
// LegacyTopicNameConfig provides topic name information required for the topic name resolution.
type LegacyTopicNameConfig struct {
TopicName string
}
// LegacyAuditApi is an implementation of AuditApi to send events to the legacy audit log system.
//
// Note: The implementation will be deprecated and replaced with the "routableAuditApi" once the new audit log routing is implemented
type LegacyAuditApi struct {
messagingApi *messaging.Api
topicNameResolver *TopicNameResolver
validator *ProtobufValidator
}
// NewLegacyAuditApi 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 NewLegacyAuditApi(
messagingApi *messaging.Api,
topicNameConfig LegacyTopicNameConfig,
validator ProtobufValidator,
) (*AuditApi, error) {
if messagingApi == nil {
return nil, ErrMessagingApiNil
}
// Topic resolver
if topicNameConfig.TopicName == "" {
return nil, errors.New("topic name is required")
}
var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: topicNameConfig.TopicName}
// Audit api
var auditApi AuditApi = &LegacyAuditApi{
messagingApi: messagingApi,
topicNameResolver: &topicNameResolver,
validator: &validator,
}
return &auditApi, nil
}
// Log implements AuditApi.Log
func (a *LegacyAuditApi) 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 *LegacyAuditApi) 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 *LegacyAuditApi) 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 *LegacyAuditApi) 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
}
// Reject event type data-access as the downstream services
// cannot handle it at the moment
if strings.HasSuffix(event.LogName, string(EventTypeDataAccess)) {
return nil, ErrUnsupportedEventTypeDataAccess
}
// 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: ContentTypeCloudEventsJson,
DataType: DataTypeLegacyAuditEventV1,
Subject: event.ProtoPayload.ResourceName,
Data: legacyBytes,
TraceParent: traceParent,
TraceState: traceState,
}
return &message, nil
}
// Send implements AuditApi.Send
func (a *LegacyAuditApi) Send(
ctx context.Context,
routableIdentifier *RoutableIdentifier,
cloudEvent *CloudEvent,
) error {
return send(a.topicNameResolver, a.messagingApi, ctx, routableIdentifier, cloudEvent)
}

View file

@ -0,0 +1,312 @@
package api
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"time"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"google.golang.org/protobuf/encoding/protojson"
)
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) {
// Event type
var eventType string
if strings.HasSuffix(event.LogName, string(EventTypeAdminActivity)) {
eventType = "ADMIN_ACTIVITY"
} else if strings.HasSuffix(event.LogName, string(EventTypeSystemEvent)) {
eventType = "SYSTEM_EVENT"
} else if strings.HasSuffix(event.LogName, string(EventTypePolicyDenied)) {
eventType = "POLICY_DENIED"
} else if strings.HasSuffix(event.LogName, string(EventTypeDataAccess)) {
return nil, ErrUnsupportedEventTypeDataAccess
} else {
return nil, errors.New("unsupported event type")
}
// 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{}{}
unescapedQuery, err := url.QueryUnescape(*event.ProtoPayload.RequestMetadata.RequestAttributes.Query)
if err != nil {
return nil, err
}
parsedUrl, err := url.Parse(fmt.Sprintf("%s?%s",
event.ProtoPayload.RequestMetadata.RequestAttributes.Path,
unescapedQuery))
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
switch routableEvent.ObjectIdentifier.Type {
case string(ObjectTypeProject):
messageContext = &LegacyAuditEventContext{
OrganizationId: nil,
FolderId: nil,
ProjectId: &routableEvent.ObjectIdentifier.Identifier,
}
case string(ObjectTypeFolder):
messageContext = &LegacyAuditEventContext{
OrganizationId: nil,
FolderId: &routableEvent.ObjectIdentifier.Identifier,
ProjectId: nil,
}
case string(ObjectTypeOrganization):
messageContext = &LegacyAuditEventContext{
OrganizationId: &routableEvent.ObjectIdentifier.Identifier,
FolderId: nil,
ProjectId: nil,
}
case string(ObjectTypeSystem):
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
serializedRequestAttributes, err := protojson.Marshal(event.ProtoPayload.Metadata)
if err != nil {
return nil, err
}
var details map[string]interface{}
err = json.Unmarshal(serializedRequestAttributes, &details)
if err != nil {
return nil, err
}
// 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"`
Request LegacyAuditEventRequest `json:"request"`
ServiceAccountDelegationInfo *LegacyAuditEventServiceAccountDelegationInfo `json:"serviceAccountDelegationInfo"`
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,21 @@
package api
import (
"testing"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"github.com/stretchr/testify/assert"
)
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,160 @@
package api
import (
"context"
"errors"
"fmt"
"strings"
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/messaging"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"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
}
// Reject event type data-access as the downstream services
// cannot handle it at the moment
if strings.HasSuffix(event.LogName, string(EventTypeDataAccess)) {
return nil, ErrUnsupportedEventTypeDataAccess
}
// 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: ContentTypeCloudEventsJson,
DataType: DataTypeLegacyAuditEventV1,
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,528 @@
package api
import (
"context"
"encoding/json"
"errors"
"net/url"
"strings"
"testing"
"time"
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/messaging"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"github.com/bufbuild/protovalidate-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestDynamicLegacyAuditApi(t *testing.T) {
// Specify test timeout
ctx, cancelFn := context.WithTimeout(context.Background(), 120*time.Second)
defer cancelFn()
// Start solace docker container
solaceContainer, err := messaging.NewSolaceContainer(context.Background())
assert.NoError(t, err)
defer solaceContainer.Stop()
// Instantiate the messaging api
messagingApi, err := messaging.NewAmqpApi(messaging.AmqpConfig{URL: solaceContainer.AmqpConnectionString})
assert.NoError(t, err)
// Validator
validator, err := protovalidate.New()
assert.NoError(t, err)
topicSubscriptionTopicPattern := "audit-log/>"
traceParent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
traceState := "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"
// Check that event-type data-access is rejected as it is currently
// not supported by downstream services
t.Run("reject data access event", func(t *testing.T) {
defer solaceContainer.StopOnError()
// Create the queue and topic subscription in solace
queueName := "reject-data-access-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-rejected"
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)
event.LogName = strings.Replace(event.LogName, string(EventTypeAdminActivity), string(EventTypeDataAccess), 1)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
ctx := context.WithValue(ctx, ContextKeyTopic, topicName)
assert.ErrorIs(t, (*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
), ErrUnsupportedEventTypeDataAccess)
})
// Check logging of organization events
t.Run("Log public organization event", func(t *testing.T) {
defer solaceContainer.StopOnError()
// Create the queue and topic subscription in solace
queueName := "org-event-public-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 with identifier
t.Run("Log private project system event", func(t *testing.T) {
defer solaceContainer.StopOnError()
queueName := "project-system-event-private"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
topicName := "topic://audit-log/eu01/v1/resource-manager/project-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 := newProjectSystemAuditEvent(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.ResourceName, *auditEvent.ResourceName)
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)
})
// 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)
escapedQuery := url.QueryEscape("param=value")
event.ProtoPayload.RequestMetadata.RequestAttributes.Query = &escapedQuery
// 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

@ -0,0 +1,671 @@
package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"testing"
"time"
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/messaging"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"github.com/Azure/go-amqp"
"github.com/bufbuild/protovalidate-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestLegacyAuditApi(t *testing.T) {
// Specify test timeout
ctx, cancelFn := context.WithTimeout(context.Background(), 120*time.Second)
defer cancelFn()
// Start solace docker container
solaceContainer, err := messaging.NewSolaceContainer(context.Background())
assert.NoError(t, err)
defer solaceContainer.Stop()
// Instantiate the messaging api
messagingApi, err := messaging.NewAmqpApi(messaging.AmqpConfig{URL: solaceContainer.AmqpConnectionString})
assert.NoError(t, err)
// Validator
validator, err := protovalidate.New()
assert.NoError(t, err)
topicSubscriptionTopicPattern := "audit-log/>"
traceParent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
traceState := "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"
// Check that event-type data-access is rejected as it is currently
// not supported by downstream services
t.Run("reject data access event", func(t *testing.T) {
defer solaceContainer.StopOnError()
// Create the queue and topic subscription in solace
queueName := "org-reject-data-access-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-rejected"
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
// Instantiate audit api
auditApi, err := NewLegacyAuditApi(
messagingApi,
LegacyTopicNameConfig{TopicName: topicName},
validator,
)
assert.NoError(t, err)
// Instantiate test data
event, objectIdentifier := newOrganizationAuditEvent(nil)
event.LogName = strings.Replace(event.LogName, string(EventTypeAdminActivity), string(EventTypeDataAccess), 1)
// Log the event to solace
assert.ErrorIs(t, (*auditApi).LogWithTrace(
ctx,
event,
auditV1.Visibility_VISIBILITY_PUBLIC,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
), ErrUnsupportedEventTypeDataAccess)
})
// Check logging of organization events
t.Run("Log public organization event", func(t *testing.T) {
defer solaceContainer.StopOnError()
// Create the queue and topic subscription in solace
queueName := "org-event-public-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 := NewLegacyAuditApi(
messagingApi,
LegacyTopicNameConfig{TopicName: topicName},
validator,
)
assert.NoError(t, err)
// Instantiate test data
event, objectIdentifier := newOrganizationAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
assert.NoError(t, (*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
))
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
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 := NewLegacyAuditApi(
messagingApi,
LegacyTopicNameConfig{TopicName: topicName},
validator,
)
assert.NoError(t, err)
// Instantiate test data
event, objectIdentifier := newOrganizationAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
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 := NewLegacyAuditApi(
messagingApi,
LegacyTopicNameConfig{TopicName: topicName},
validator,
)
assert.NoError(t, err)
// Instantiate test data
event, objectIdentifier := newFolderAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
assert.NoError(t, (*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
))
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
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 := NewLegacyAuditApi(
messagingApi,
LegacyTopicNameConfig{TopicName: topicName},
validator,
)
assert.NoError(t, err)
// Instantiate test data
event, objectIdentifier := newFolderAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
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 := NewLegacyAuditApi(
messagingApi,
LegacyTopicNameConfig{TopicName: topicName},
validator,
)
assert.NoError(t, err)
// Instantiate test data
event, objectIdentifier := newProjectAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
assert.NoError(t, (*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
))
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 := NewLegacyAuditApi(
messagingApi,
LegacyTopicNameConfig{TopicName: topicName},
validator,
)
assert.NoError(t, err)
// Instantiate test data
event, objectIdentifier := newProjectAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
assert.NoError(t, (*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
))
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentMessage(t, topicName, message, event, &traceParent, &traceState)
})
// Check logging of system events with identifier
t.Run("Log private project system event", func(t *testing.T) {
defer solaceContainer.StopOnError()
queueName := "project-system-event-private"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
topicName := "topic://audit-log/eu01/v1/resource-manager/project-system-changed"
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
// Instantiate audit api
auditApi, err := NewLegacyAuditApi(
messagingApi,
LegacyTopicNameConfig{TopicName: topicName},
validator,
)
assert.NoError(t, err)
// Instantiate test data
event := newProjectSystemAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
assert.NoError(t,
(*auditApi).LogWithTrace(
ctx,
event,
visibility,
RoutableSystemIdentifier,
nil,
nil,
))
// Receive the event from solace
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
// Check topic name
assert.Equal(t, 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.ResourceName, *auditEvent.ResourceName)
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)
})
// 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 := NewLegacyAuditApi(
messagingApi,
LegacyTopicNameConfig{TopicName: topicName},
validator,
)
assert.NoError(t, err)
// Instantiate test data
event := newSystemAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
assert.NoError(t,
(*auditApi).LogWithTrace(
ctx,
event,
visibility,
RoutableSystemIdentifier,
nil,
nil,
))
// Receive the event from solace
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
// Check topic name
assert.Equal(t, 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 := NewLegacyAuditApi(
messagingApi,
LegacyTopicNameConfig{TopicName: topicName},
validator,
)
assert.NoError(t, err)
// Instantiate test data
event, objectIdentifier := newOrganizationAuditEvent(nil)
escapedQuery := url.QueryEscape("param=value")
event.ProtoPayload.RequestMetadata.RequestAttributes.Query = &escapedQuery
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
assert.NoError(t, (*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
))
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentMessageWithDetails(t, topicName, message, event, &traceParent, &traceState)
})
}
func validateSentMessage(
t *testing.T,
topicName string,
message *amqp.Message,
event *auditV1.AuditLogEntry,
traceParent *string,
traceState *string,
) {
// Check message properties
assert.Equal(t, topicName, *message.Properties.To)
assert.Equal(t, *traceParent, message.ApplicationProperties["cloudEvents:traceparent"])
assert.Equal(t, *traceState, message.ApplicationProperties["cloudEvents:tracestate"])
// Check deserialized message
var auditEvent LegacyAuditEvent
assert.NoError(t, json.Unmarshal(message.Data[0], &auditEvent))
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:
assert.Fail(t, "unknown log severity")
}
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, "ADMIN_ACTIVITY", auditEvent.EventType)
assert.Equal(t, severity, 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)
}
func validateSentMessageWithDetails(
t *testing.T,
topicName string,
message *amqp.Message,
event *auditV1.AuditLogEntry,
traceParent *string,
traceState *string,
) {
// Check topic name
assert.Equal(t, topicName, *message.Properties.To)
assert.Equal(t, ContentTypeCloudEventsJson, message.ApplicationProperties["cloudEvents:datacontenttype"])
assert.Equal(t, DataTypeLegacyAuditEventV1, message.ApplicationProperties["cloudEvents:type"])
assert.Equal(t, *traceParent, message.ApplicationProperties["cloudEvents:traceparent"])
assert.Equal(t, *traceState, 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, "ADMIN_ACTIVITY", auditEvent.EventType)
assert.Equal(t, "INFO", auditEvent.Severity)
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Path, auditEvent.Request.Endpoint)
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{}{}
unescapedQuery, err := url.QueryUnescape(*event.ProtoPayload.RequestMetadata.RequestAttributes.Query)
assert.NoError(t, err)
parsedUrl, err := url.Parse(fmt.Sprintf("%s?%s",
event.ProtoPayload.RequestMetadata.RequestAttributes.Path,
unescapedQuery))
assert.NoError(t, err)
for k, v := range parsedUrl.Query() {
parameters[k] = v
}
}
assert.Equal(t, event.ProtoPayload.Request.AsMap(), *auditEvent.Request.Body)
assert.Equal(t, parameters, *auditEvent.Request.Parameters)
for key, value := range event.ProtoPayload.RequestMetadata.RequestAttributes.Headers {
assert.Equal(t, value, (*auditEvent.Request.Headers)[key])
}
assert.Equal(t, event.ProtoPayload.RequestMetadata.CallerIp, auditEvent.SourceIpAddress)
assert.Equal(t, event.ProtoPayload.RequestMetadata.CallerSuppliedUserAgent, auditEvent.UserAgent)
assert.Equal(t, event.ProtoPayload.Request.AsMap(), *auditEvent.Details)
assert.Equal(t, event.ProtoPayload.Response.AsMap(), *auditEvent.Result)
assert.True(t, auditEvent.Context.OrganizationId != nil || auditEvent.Context.FolderId != nil || auditEvent.Context.ProjectId != nil)
for idx, principal := range event.ProtoPayload.AuthenticationInfo.ServiceAccountDelegationInfo {
switch principalValue := principal.Authority.(type) {
case *auditV1.ServiceAccountDelegationInfo_IdpPrincipal_:
assert.Equal(t, principalValue.IdpPrincipal.PrincipalId, auditEvent.ServiceAccountDelegationInfo.Principals[idx].Id)
assert.Equal(t, principalValue.IdpPrincipal.PrincipalEmail, auditEvent.ServiceAccountDelegationInfo.Principals[idx].Email)
case *auditV1.ServiceAccountDelegationInfo_SystemPrincipal_:
assert.Equal(t, "system", auditEvent.ServiceAccountDelegationInfo.Principals[idx].Id)
}
}
}
func TestLegacyAuditApi_NewLegacyAuditApi(t *testing.T) {
t.Run("messaging api nil", func(t *testing.T) {
auditApi, err := NewLegacyAuditApi(nil, LegacyTopicNameConfig{}, nil)
assert.Nil(t, auditApi)
assert.EqualError(t, err, "messaging api nil")
})
t.Run("topic name is blank", func(t *testing.T) {
// 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)
auditApi, err := NewLegacyAuditApi(messagingApi, LegacyTopicNameConfig{
TopicName: "",
}, validator)
assert.Nil(t, auditApi)
assert.EqualError(t, err, "topic name is required")
})
}
func TestLegacyAuditApi_ValidateAndSerialize_ValidationFailed(t *testing.T) {
expectedError := errors.New("expected error")
validator := &ProtobufValidatorMock{}
validator.On("Validate", mock.Anything).Return(expectedError)
var protobufValidator ProtobufValidator = validator
auditApi := LegacyAuditApi{validator: &protobufValidator}
event := newSystemAuditEvent(nil)
_, err := auditApi.ValidateAndSerialize(event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
assert.ErrorIs(t, err, expectedError)
}
func TestLegacyAuditApi_Log_ValidationFailed(t *testing.T) {
expectedError := errors.New("expected error")
validator := &ProtobufValidatorMock{}
validator.On("Validate", mock.Anything).Return(expectedError)
var protobufValidator ProtobufValidator = validator
auditApi := LegacyAuditApi{validator: &protobufValidator}
event := newSystemAuditEvent(nil)
err := auditApi.Log(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
assert.ErrorIs(t, err, expectedError)
}
func TestLegacyAuditApi_Log_NilEvent(t *testing.T) {
auditApi := LegacyAuditApi{}
err := auditApi.Log(context.Background(), nil, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
assert.ErrorIs(t, err, ErrEventNil)
}
func TestLegacyAuditApi_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 := LegacyAuditApi{validator: &protobufValidator}
_, err := auditApi.ValidateAndSerialize(event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier))
assert.ErrorIs(t, err, ErrUnsupportedRoutableType)
}

111
audit/api/api_mock.go Normal file
View file

@ -0,0 +1,111 @@
package api
import (
"context"
"fmt"
"strings"
"google.golang.org/protobuf/proto"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"github.com/bufbuild/protovalidate-go"
)
// MockAuditApi is an implementation of AuditApi that does nothing and has no dependency to external systems.
type MockAuditApi struct {
validator *ProtobufValidator
}
func NewMockAuditApi() (*AuditApi, error) {
validator, err := protovalidate.New()
if err != nil {
return nil, err
}
var protobufValidator ProtobufValidator = validator
var auditApi AuditApi = &MockAuditApi{validator: &protobufValidator}
return &auditApi, nil
}
// Log implements AuditApi.Log.
// Validates and serializes the event but doesn't send it.
func (a *MockAuditApi) 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.
// Validates and serializes the event but doesn't send it.
func (a *MockAuditApi) LogWithTrace(
_ context.Context,
event *auditV1.AuditLogEntry,
visibility auditV1.Visibility,
routableIdentifier *RoutableIdentifier,
traceParent *string,
traceState *string,
) error {
_, err := a.ValidateAndSerializeWithTrace(event, visibility, routableIdentifier, traceParent, traceState)
return err
}
// ValidateAndSerialize implements AuditApi.ValidateAndSerialize
func (a *MockAuditApi) ValidateAndSerialize(
event *auditV1.AuditLogEntry,
visibility auditV1.Visibility,
routableIdentifier *RoutableIdentifier,
) (*CloudEvent, error) {
return a.ValidateAndSerializeWithTrace(event, visibility, routableIdentifier, nil, nil)
}
// ValidateAndSerializeWithTrace implements AuditApi.ValidateAndSerializeWithTrace
func (a *MockAuditApi) 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
}
// Reject event type data-access as the downstream services
// cannot handle it at the moment
if strings.HasSuffix(event.LogName, string(EventTypeDataAccess)) {
return nil, ErrUnsupportedEventTypeDataAccess
}
routableEventBytes, err := proto.Marshal(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: "application/cloudevents+protobuf",
DataType: fmt.Sprintf("%v", routableEvent.ProtoReflect().Descriptor().FullName()),
Subject: event.ProtoPayload.ResourceName,
Data: routableEventBytes,
TraceParent: traceParent,
TraceState: traceState,
}
return &message, nil
}
// Send implements AuditApi.Send
func (a *MockAuditApi) Send(context.Context, *RoutableIdentifier, *CloudEvent) error {
return nil
}

View file

@ -0,0 +1,61 @@
package api
import (
"context"
"strings"
"testing"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"github.com/stretchr/testify/assert"
)
func TestMockAuditApi_Log(t *testing.T) {
auditApi, err := NewMockAuditApi()
assert.NoError(t, err)
// Instantiate test data
event, objectIdentifier := newOrganizationAuditEvent(nil)
routableObjectIdentifier := NewRoutableIdentifier(objectIdentifier)
// Test
t.Run("Log", func(t *testing.T) {
assert.Nil(t, (*auditApi).Log(
context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, routableObjectIdentifier))
})
t.Run("reject data access event", func(t *testing.T) {
event, objectIdentifier := newOrganizationAuditEvent(nil)
event.LogName = strings.Replace(event.LogName, string(EventTypeAdminActivity), string(EventTypeDataAccess), 1)
routableObjectIdentifier := NewRoutableIdentifier(objectIdentifier)
assert.ErrorIs(t, (*auditApi).Log(
context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, routableObjectIdentifier),
ErrUnsupportedEventTypeDataAccess)
})
t.Run("ValidateAndSerialize", func(t *testing.T) {
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
cloudEvent, err := (*auditApi).ValidateAndSerializeWithTrace(
event, visibility, routableObjectIdentifier, nil, nil)
assert.NoError(t, err)
validateRoutableEventPayload(
t, cloudEvent.Data, objectIdentifier, event, event.ProtoPayload.OperationName, visibility)
})
t.Run("ValidateAndSerialize event nil", func(t *testing.T) {
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
_, err := (*auditApi).ValidateAndSerialize(nil, visibility, routableObjectIdentifier)
assert.ErrorIs(t, err, ErrEventNil)
})
t.Run("Send", func(t *testing.T) {
var cloudEvent = CloudEvent{}
assert.Nil(t, (*auditApi).Send(context.Background(), routableObjectIdentifier, &cloudEvent))
})
}

198
audit/api/api_routable.go Normal file
View file

@ -0,0 +1,198 @@
package api
import (
"context"
"errors"
"fmt"
"strings"
"google.golang.org/protobuf/proto"
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/messaging"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
)
// routableTopicNameResolver implements TopicNameResolver.
// Resolves topic names by concatenating topic type prefixes with routing identifiers.
type routableTopicNameResolver struct {
folderTopicPrefix string
organizationTopicPrefix string
projectTopicPrefix string
// If no identifier is provided for routing, it will be routed to a system topic
systemTopicName string
}
// Resolve implements TopicNameResolver.Resolve.
func (r *routableTopicNameResolver) Resolve(routableIdentifier *RoutableIdentifier) (string, error) {
if routableIdentifier == nil {
return "", ErrObjectIdentifierNil
}
switch routableIdentifier.Type {
case ObjectTypeOrganization:
return fmt.Sprintf("topic://%s/%s", r.organizationTopicPrefix, routableIdentifier.Identifier), nil
case ObjectTypeProject:
return fmt.Sprintf("topic://%s/%s", r.projectTopicPrefix, routableIdentifier.Identifier), nil
case ObjectTypeFolder:
return fmt.Sprintf("topic://%s/%s", r.folderTopicPrefix, routableIdentifier.Identifier), nil
case ObjectTypeSystem:
return r.systemTopicName, nil
default:
return "", ErrUnsupportedObjectIdentifierType
}
}
// topicNameConfig provides topic name information required for the topic name resolution.
type topicNameConfig struct {
FolderTopicPrefix string
OrganizationTopicPrefix string
ProjectTopicPrefix string
SystemTopicName string
}
// routableAuditApi is an implementation of AuditApi.
// Warning: It is only there for local (compatibility) testing.
// DO NOT USE IT!
type routableAuditApi struct {
messagingApi *messaging.Api
topicNameResolver *TopicNameResolver
validator *ProtobufValidator
}
// NewRoutableAuditApi can be used to initialize the audit log api.
func newRoutableAuditApi(
messagingApi *messaging.Api,
topicNameConfig topicNameConfig,
validator ProtobufValidator,
) (*AuditApi, error) {
if messagingApi == nil {
return nil, errors.New("messaging api nil")
}
// Topic resolver
if topicNameConfig.FolderTopicPrefix == "" {
return nil, errors.New("folder topic prefix is required")
}
if topicNameConfig.OrganizationTopicPrefix == "" {
return nil, errors.New("organization topic prefix is required")
}
if topicNameConfig.ProjectTopicPrefix == "" {
return nil, errors.New("project topic prefix is required")
}
if topicNameConfig.SystemTopicName == "" {
return nil, errors.New("system topic name is required")
}
var topicNameResolver TopicNameResolver = &routableTopicNameResolver{
folderTopicPrefix: topicNameConfig.FolderTopicPrefix,
organizationTopicPrefix: topicNameConfig.OrganizationTopicPrefix,
projectTopicPrefix: topicNameConfig.ProjectTopicPrefix,
systemTopicName: topicNameConfig.SystemTopicName,
}
// Audit api
var auditApi AuditApi = &routableAuditApi{
messagingApi: messagingApi,
topicNameResolver: &topicNameResolver,
validator: &validator,
}
return &auditApi, nil
}
// Log implements AuditApi.Log
func (a *routableAuditApi) 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 *routableAuditApi) 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
func (a *routableAuditApi) ValidateAndSerialize(
event *auditV1.AuditLogEntry,
visibility auditV1.Visibility,
routableIdentifier *RoutableIdentifier,
) (*CloudEvent, error) {
return a.ValidateAndSerializeWithTrace(event, visibility, routableIdentifier, nil, nil)
}
// ValidateAndSerializeWithTrace implements AuditApi.ValidateAndSerializeWithTrace
func (a *routableAuditApi) 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
}
// Reject event type data-access as the downstream services
// cannot handle it at the moment
if strings.HasSuffix(event.LogName, string(EventTypeDataAccess)) {
return nil, ErrUnsupportedEventTypeDataAccess
}
routableEventBytes, err := proto.Marshal(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: routableEventBytes,
TraceParent: traceParent,
TraceState: traceState,
}
return &message, nil
}
// Send implements AuditApi.Send
func (a *routableAuditApi) Send(
ctx context.Context,
routableIdentifier *RoutableIdentifier,
cloudEvent *CloudEvent,
) error {
return send(a.topicNameResolver, a.messagingApi, ctx, routableIdentifier, cloudEvent)
}

View file

@ -0,0 +1,574 @@
package api
import (
"context"
"errors"
"fmt"
"strings"
"testing"
"time"
"github.com/google/uuid"
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/messaging"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"github.com/Azure/go-amqp"
"github.com/bufbuild/protovalidate-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"google.golang.org/protobuf/proto"
)
func TestRoutableAuditApi(t *testing.T) {
// Specify test timeout
ctx, cancelFn := context.WithTimeout(context.Background(), 120*time.Second)
defer cancelFn()
// Start solace docker container
solaceContainer, err := messaging.NewSolaceContainer(context.Background())
assert.NoError(t, err)
defer solaceContainer.Stop()
// Instantiate the messaging api
messagingApi, err := messaging.NewAmqpApi(messaging.AmqpConfig{URL: solaceContainer.AmqpConnectionString})
assert.NoError(t, err)
// Validator
validator, err := protovalidate.New()
assert.NoError(t, err)
traceParent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
traceState := "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"
// Instantiate the audit api
organizationTopicPrefix := "org"
projectTopicPrefix := "project"
folderTopicPrefix := "folder"
systemTopicName := "topic://system/admin-events"
auditApi, err := newRoutableAuditApi(
messagingApi,
topicNameConfig{
FolderTopicPrefix: folderTopicPrefix,
OrganizationTopicPrefix: organizationTopicPrefix,
ProjectTopicPrefix: projectTopicPrefix,
SystemTopicName: systemTopicName},
validator,
)
assert.NoError(t, err)
// Check that event-type data-access is rejected as it is currently
// not supported by downstream services
t.Run("reject data access event", func(t *testing.T) {
defer solaceContainer.StopOnError()
// Create the queue and topic subscription in solace
queueName := "org-reject-data-access"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "org/*"))
// Instantiate test data
event, objectIdentifier := newOrganizationAuditEvent(nil)
event.LogName = strings.Replace(event.LogName, string(EventTypeAdminActivity), string(EventTypeDataAccess), 1)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
assert.ErrorIs(t, (*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
), ErrUnsupportedEventTypeDataAccess)
})
// Check logging of organization events
t.Run("Log public organization event", func(t *testing.T) {
defer solaceContainer.StopOnError()
// Create the queue and topic subscription in solace
queueName := "org-event-public"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "org/*"))
// Instantiate test data
event, objectIdentifier := newOrganizationAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
assert.NoError(t, (*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
))
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentEvent(
t,
organizationTopicPrefix,
message,
objectIdentifier,
event,
"stackit.resourcemanager.v2.organization.created",
visibility,
&traceParent,
&traceState)
})
t.Run("Log private organization event", func(t *testing.T) {
defer solaceContainer.StopOnError()
queueName := "org-event-private"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "org/*"))
// Instantiate test data
event, objectIdentifier := newOrganizationAuditEvent(nil)
topicName := fmt.Sprintf("org/%s", objectIdentifier.Identifier)
assert.NoError(
t,
solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicName))
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
assert.NoError(t,
(*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
))
// Receive the event from solace
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentEvent(
t,
organizationTopicPrefix,
message,
objectIdentifier,
event,
"stackit.resourcemanager.v2.organization.created",
visibility,
&traceParent,
&traceState)
})
// Check logging of folder events
t.Run("Log public folder event", func(t *testing.T) {
defer solaceContainer.StopOnError()
// Create the queue and topic subscription in solace
queueName := "folder-event-public"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "folder/*"))
// Instantiate test data
event, objectIdentifier := newFolderAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
assert.NoError(t, (*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
))
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentEvent(
t,
folderTopicPrefix,
message,
objectIdentifier,
event,
"stackit.resourcemanager.v2.folder.created",
visibility,
&traceParent,
&traceState)
})
t.Run("Log private folder event", func(t *testing.T) {
defer solaceContainer.StopOnError()
queueName := "folder-event-private"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "folder/*"))
// Instantiate test data
event, objectIdentifier := newFolderAuditEvent(nil)
topicName := fmt.Sprintf("folder/%s", objectIdentifier.Identifier)
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicName))
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
assert.NoError(t,
(*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
))
// Receive the event from solace
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentEvent(
t,
folderTopicPrefix,
message,
objectIdentifier,
event,
"stackit.resourcemanager.v2.folder.created",
visibility,
&traceParent,
&traceState)
})
// Check logging of project events
t.Run("Log public project event", func(t *testing.T) {
defer solaceContainer.StopOnError()
queueName := "project-event-public"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "project/*"))
// Instantiate test data
event, objectIdentifier := newProjectAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
assert.NoError(t,
(*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
))
// Receive the event from solace
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentEvent(
t,
projectTopicPrefix,
message,
objectIdentifier,
event,
"stackit.resourcemanager.v2.project.created",
visibility,
&traceParent,
&traceState)
})
t.Run("Log private project event", func(t *testing.T) {
defer solaceContainer.StopOnError()
queueName := "project-event-private"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "project/*"))
// Instantiate test data
event, objectIdentifier := newProjectAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
assert.NoError(t,
(*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
))
// Receive the event from solace
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentEvent(
t,
projectTopicPrefix,
message,
objectIdentifier,
event,
"stackit.resourcemanager.v2.project.created",
visibility,
&traceParent,
&traceState)
})
// Check logging of system events with identifier
t.Run("Log private project system event", func(t *testing.T) {
defer solaceContainer.StopOnError()
queueName := "project-system-event-private"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "system/*"))
// Instantiate test data
event := newProjectSystemAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
assert.NoError(t,
(*auditApi).LogWithTrace(
ctx,
event,
visibility,
RoutableSystemIdentifier,
nil,
nil,
))
// Receive the event from solace
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
// Check topic name
assert.Equal(t, systemTopicName, *message.Properties.To)
// Check cloud event properties
applicationProperties := message.ApplicationProperties
assert.Equal(t, "1.0", applicationProperties["cloudEvents:specversion"])
assert.Equal(t, "resource-manager", applicationProperties["cloudEvents:source"])
_, isUuid := uuid.Parse(fmt.Sprintf("%s", applicationProperties["cloudEvents:id"]))
assert.True(t, true, isUuid)
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime().UnixMilli(), applicationProperties["cloudEvents:time"])
assert.Equal(t, "application/cloudevents+protobuf", applicationProperties["cloudEvents:datacontenttype"])
assert.Equal(t, "audit.v1.RoutableAuditEvent", applicationProperties["cloudEvents:type"])
assert.Nil(t, applicationProperties["cloudEvents:traceparent"])
assert.Nil(t, applicationProperties["cloudEvents:tracestate"])
// Check deserialized message
validateRoutableEventPayload(
t,
message.Data[0],
RoutableSystemIdentifier.ToObjectIdentifier(),
event,
"stackit.resourcemanager.v2.system.changed",
visibility)
})
// Check logging of system events
t.Run("Log private system event", func(t *testing.T) {
defer solaceContainer.StopOnError()
queueName := "system-event-private"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "system/*"))
// Instantiate test data
event := newSystemAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
assert.NoError(t,
(*auditApi).LogWithTrace(
ctx,
event,
visibility,
RoutableSystemIdentifier,
nil,
nil,
))
// Receive the event from solace
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
// Check topic name
assert.Equal(t, systemTopicName, *message.Properties.To)
// Check cloud event properties
applicationProperties := message.ApplicationProperties
assert.Equal(t, "1.0", applicationProperties["cloudEvents:specversion"])
assert.Equal(t, "resource-manager", applicationProperties["cloudEvents:source"])
_, isUuid := uuid.Parse(fmt.Sprintf("%s", applicationProperties["cloudEvents:id"]))
assert.True(t, true, isUuid)
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime().UnixMilli(), applicationProperties["cloudEvents:time"])
assert.Equal(t, "application/cloudevents+protobuf", applicationProperties["cloudEvents:datacontenttype"])
assert.Equal(t, "audit.v1.RoutableAuditEvent", applicationProperties["cloudEvents:type"])
assert.Nil(t, applicationProperties["cloudEvents:traceparent"])
assert.Nil(t, applicationProperties["cloudEvents:tracestate"])
// Check deserialized message
validateRoutableEventPayload(
t,
message.Data[0],
SystemIdentifier,
event,
"stackit.resourcemanager.v2.system.changed",
visibility)
})
// Check logging of organization events
t.Run("Log event with details", func(t *testing.T) {
defer solaceContainer.StopOnError()
// Create the queue and topic subscription in solace
queueName := "org-event-with-details"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "org/*"))
// Instantiate test data
event, objectIdentifier := newOrganizationAuditEvent(nil)
// Log the event to solace
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
assert.NoError(t, (*auditApi).LogWithTrace(
ctx,
event,
visibility,
NewRoutableIdentifier(objectIdentifier),
&traceParent,
&traceState,
))
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
assert.NoError(t, err)
validateSentEvent(
t,
organizationTopicPrefix,
message,
objectIdentifier,
event,
"stackit.resourcemanager.v2.organization.created",
visibility,
&traceParent,
&traceState)
})
}
func validateSentEvent(
t *testing.T,
topicPrefix string,
message *amqp.Message,
objectIdentifier *auditV1.ObjectIdentifier,
event *auditV1.AuditLogEntry,
operationName string,
visibility auditV1.Visibility,
traceParent *string,
traceState *string,
) {
// Check topic name
assert.Equal(t,
fmt.Sprintf("topic://%s/%s", topicPrefix, objectIdentifier.Identifier),
*message.Properties.To)
// Check cloud event properties
applicationProperties := message.ApplicationProperties
assert.Equal(t, "1.0", applicationProperties["cloudEvents:specversion"])
assert.Equal(t, "resource-manager", applicationProperties["cloudEvents:source"])
_, isUuid := uuid.Parse(fmt.Sprintf("%s", applicationProperties["cloudEvents:id"]))
assert.True(t, true, isUuid)
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime().UnixMilli(), applicationProperties["cloudEvents:time"])
assert.Equal(t, ContentTypeCloudEventsProtobuf, applicationProperties["cloudEvents:datacontenttype"])
assert.Equal(t, "audit.v1.RoutableAuditEvent", applicationProperties["cloudEvents:type"])
assert.Equal(t, *traceParent, applicationProperties["cloudEvents:traceparent"])
assert.Equal(t, *traceState, applicationProperties["cloudEvents:tracestate"])
// Check deserialized message
validateRoutableEventPayload(
t, message.Data[0], objectIdentifier, event, operationName, visibility)
}
func validateRoutableEventPayload(
t *testing.T,
payload []byte,
objectIdentifier *auditV1.ObjectIdentifier,
event *auditV1.AuditLogEntry,
operationName string,
visibility auditV1.Visibility,
) {
// Check routable audit event parameters
var routableAuditEvent auditV1.RoutableAuditEvent
assert.NoError(t, proto.Unmarshal(payload, &routableAuditEvent))
assert.Equal(t, operationName, routableAuditEvent.OperationName)
assert.Equal(t, visibility, routableAuditEvent.Visibility)
assert.True(t, proto.Equal(objectIdentifier, routableAuditEvent.ObjectIdentifier))
var auditEvent auditV1.AuditLogEntry
switch data := routableAuditEvent.Data.(type) {
case *auditV1.RoutableAuditEvent_UnencryptedData:
assert.NoError(t, proto.Unmarshal(data.UnencryptedData.Data, &auditEvent))
default:
assert.Fail(t, "Encrypted data not expected")
}
// Check audit event
assert.True(t, proto.Equal(event, &auditEvent))
}
func TestRoutableTopicNameResolver_Resolve_UnsupportedIdentifierType(t *testing.T) {
resolver := routableTopicNameResolver{}
_, err := resolver.Resolve(NewRoutableIdentifier(&auditV1.ObjectIdentifier{Type: "unsupported"}))
assert.ErrorIs(t, err, ErrUnsupportedObjectIdentifierType)
}
func TestNewRoutableAuditApi_NewRoutableAuditApi_MessagingApiNil(t *testing.T) {
auditApi, err := newRoutableAuditApi(nil, topicNameConfig{}, nil)
assert.Nil(t, auditApi)
assert.EqualError(t, err, "messaging api nil")
}
func TestRoutableAuditApi_ValidateAndSerialize_ValidationFailed(t *testing.T) {
expectedError := errors.New("expected error")
validator := &ProtobufValidatorMock{}
validator.On("Validate", mock.Anything).Return(expectedError)
var protobufValidator ProtobufValidator = validator
auditApi := routableAuditApi{validator: &protobufValidator}
event := newSystemAuditEvent(nil)
_, err := auditApi.ValidateAndSerialize(event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
assert.ErrorIs(t, err, expectedError)
}
func TestRoutableAuditApi_Log_ValidationFailed(t *testing.T) {
expectedError := errors.New("expected error")
validator := &ProtobufValidatorMock{}
validator.On("Validate", mock.Anything).Return(expectedError)
var protobufValidator ProtobufValidator = validator
auditApi := routableAuditApi{validator: &protobufValidator}
event := newSystemAuditEvent(nil)
err := auditApi.LogWithTrace(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier, nil, nil)
assert.ErrorIs(t, err, expectedError)
}
func TestRoutableAuditApi_Log_NilEvent(t *testing.T) {
auditApi := routableAuditApi{}
err := auditApi.Log(context.Background(), nil, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
assert.ErrorIs(t, err, ErrEventNil)
}

70
audit/api/base64.go Normal file
View file

@ -0,0 +1,70 @@
package api
import (
"encoding/base64"
"encoding/json"
"errors"
"strings"
)
const base64AuditEventV1 = "v1"
var ErrBase64StringEmpty = errors.New("base64 string must not be empty")
var ErrRoutableIdentifierNil = errors.New("routableIdentifier must not be nil")
var ErrUnsupportedBase64StringVersion = errors.New("unsupported base64 cloud event string version")
type serializableEvent struct {
CloudEvent CloudEvent `json:"cloudEvent"`
RoutableIdentifier RoutableIdentifier `json:"routableIdentifier"`
}
func ToBase64(
cloudEvent *CloudEvent,
routableIdentifier *RoutableIdentifier) (*string, error) {
if cloudEvent == nil {
return nil, ErrCloudEventNil
}
if routableIdentifier == nil {
return nil, ErrRoutableIdentifierNil
}
event := serializableEvent{
CloudEvent: *cloudEvent,
RoutableIdentifier: *routableIdentifier,
}
serializedEvent, err := json.Marshal(event)
if err != nil {
return nil, err
}
base64Str := base64.StdEncoding.EncodeToString(serializedEvent)
base64Str = base64Str + base64AuditEventV1
return &base64Str, nil
}
func FromBase64(base64Str string) (*CloudEvent, *RoutableIdentifier, error) {
if base64Str == "" {
return nil, nil, ErrBase64StringEmpty
}
if !strings.HasSuffix(base64Str, base64AuditEventV1) {
return nil, nil, ErrUnsupportedBase64StringVersion
}
base64Str = strings.TrimSuffix(base64Str, base64AuditEventV1)
base64Bytes, err := base64.StdEncoding.DecodeString(base64Str)
if err != nil {
return nil, nil, err
}
event := serializableEvent{}
err = json.Unmarshal(base64Bytes, &event)
if err != nil {
return nil, nil, err
}
return &event.CloudEvent, &event.RoutableIdentifier, nil
}

85
audit/api/base64_test.go Normal file
View file

@ -0,0 +1,85 @@
package api
import (
"encoding/base64"
"github.com/stretchr/testify/assert"
"testing"
)
func Test_ToBase64(t *testing.T) {
t.Run("cloud event nil", func(t *testing.T) {
var cloudEvent *CloudEvent = nil
routableIdentifier := RoutableSystemIdentifier
base64str, err := ToBase64(cloudEvent, routableIdentifier)
assert.ErrorIs(t, err, ErrCloudEventNil)
assert.Nil(t, base64str)
})
t.Run("routable identifier nil", func(t *testing.T) {
cloudEvent := &CloudEvent{}
var routableIdentifier *RoutableIdentifier = nil
base64str, err := ToBase64(cloudEvent, routableIdentifier)
assert.ErrorIs(t, err, ErrRoutableIdentifierNil)
assert.Nil(t, base64str)
})
t.Run("encoded event", func(t *testing.T) {
e := &CloudEvent{}
r := RoutableSystemIdentifier
base64str, err := ToBase64(e, r)
assert.NoError(t, err)
cloudEvent, routableIdentifier, err := FromBase64(*base64str)
assert.NoError(t, err)
assert.Equal(t, e, cloudEvent)
assert.Equal(t, r, routableIdentifier)
})
}
func Test_FromBase64(t *testing.T) {
t.Run("empty string", func(t *testing.T) {
cloudEvent, routableIdentifier, err := FromBase64("")
assert.ErrorIs(t, err, ErrBase64StringEmpty)
assert.Nil(t, cloudEvent)
assert.Nil(t, routableIdentifier)
})
t.Run("without version suffix", func(t *testing.T) {
cloudEvent, routableIdentifier, err := FromBase64("ey")
assert.ErrorIs(t, err, ErrUnsupportedBase64StringVersion)
assert.Nil(t, cloudEvent)
assert.Nil(t, routableIdentifier)
})
t.Run("no base64 string", func(t *testing.T) {
cloudEvent, routableIdentifier, err := FromBase64("no base 64 v1")
assert.EqualError(t, err, "illegal base64 data at input byte 2")
assert.Nil(t, cloudEvent)
assert.Nil(t, routableIdentifier)
})
t.Run("no json serialized event", func(t *testing.T) {
base64Str := base64.StdEncoding.EncodeToString([]byte("not expected"))
base64Str = base64Str + base64AuditEventV1
cloudEvent, routableIdentifier, err := FromBase64(base64Str)
assert.EqualError(t, err, "invalid character 'o' in literal null (expecting 'u')")
assert.Nil(t, cloudEvent)
assert.Nil(t, routableIdentifier)
})
t.Run("decoded event", func(t *testing.T) {
e := &CloudEvent{}
r := RoutableSystemIdentifier
base64str, err := ToBase64(e, r)
assert.NoError(t, err)
cloudEvent, routableIdentifier, err := FromBase64(*base64str)
assert.NoError(t, err)
assert.Equal(t, e, cloudEvent)
assert.Equal(t, r, routableIdentifier)
})
}

662
audit/api/builder.go Normal file
View file

@ -0,0 +1,662 @@
package api
import (
"context"
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/utils"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"errors"
"fmt"
"github.com/google/uuid"
"go.opentelemetry.io/otel/trace"
"time"
)
const quadZero = "0.0.0.0"
type SequenceNumber uint64
type AuditParameters struct {
// A map that is added as "details" to the message
Details map[string]interface{}
// The type of the event
EventType EventType
// A set of user-defined (key, value) data that provides additional
// information about the log entry.
Labels map[string]string
// UUID identifier of the object, the audit event refers to
ObjectId string
// Type of the object, the audit event refers to
ObjectType ObjectType
ResponseBody any
// Log severity
Severity auditV1.LogSeverity
}
func getObjectIdAndTypeFromAuditParams(
auditParams *AuditParameters,
) (string, *ObjectType, error) {
objectId := auditParams.ObjectId
if objectId == "" {
return "", nil, errors.New("object id missing")
}
var objectType *ObjectType
if auditParams.ObjectType != "" {
objectType = &auditParams.ObjectType
}
if objectType == nil {
return "", nil, errors.New("object type missing")
}
if err := objectType.IsSupportedType(); err != nil {
return "", nil, err
}
return objectId, objectType, nil
}
// AuditLogEntryBuilder collects audit params to construct auditV1.AuditLogEntry
type AuditLogEntryBuilder struct {
auditParams AuditParameters
auditRequest AuditRequest
auditResponse AuditResponse
auditMetadata AuditMetadata
// Region and optional zone id. If both, separated with a - (dash).
// Example: eu01
location string
// The ID of the K8s Pod, Service-Instance, etc. (must be unique for a sending service)
workerId string
}
// NewAuditLogEntryBuilder returns a builder to construct auditV1.AuditLogEntry
func NewAuditLogEntryBuilder() *AuditLogEntryBuilder {
requestTime := time.Now().UTC()
return &AuditLogEntryBuilder{
auditParams: AuditParameters{
EventType: EventTypeAdminActivity,
},
auditRequest: AuditRequest{
Request: &ApiRequest{},
RequestClientIP: quadZero,
RequestCorrelationId: nil,
RequestId: nil,
RequestTime: &requestTime,
},
auditResponse: AuditResponse{
ResponseBodyBytes: nil,
ResponseStatusCode: 200,
ResponseHeaders: make(map[string][]string),
ResponseNumItems: nil,
ResponseTime: nil,
},
auditMetadata: AuditMetadata{
AuditInsertId: "",
AuditLabels: nil,
AuditLogName: "",
AuditLogSeverity: auditV1.LogSeverity_LOG_SEVERITY_DEFAULT,
AuditOperationName: "",
AuditPermission: nil,
AuditPermissionGranted: nil,
AuditResourceName: "",
AuditServiceName: "",
AuditTime: nil,
},
location: "",
workerId: "",
}
}
func (builder *AuditLogEntryBuilder) AsSystemEvent() *AuditLogEntryBuilder {
if builder.auditRequest.Request == nil {
builder.auditRequest.Request = &ApiRequest{}
}
if builder.auditRequest.Request.Header == nil {
builder.auditRequest.Request.Header = map[string][]string{"user-agent": {"none"}}
}
if builder.auditRequest.Request.Host == "" {
builder.auditRequest.Request.Host = quadZero
}
if builder.auditRequest.Request.Method == "" {
builder.auditRequest.Request.Method = "OTHER"
}
if builder.auditRequest.Request.Scheme == "" {
builder.auditRequest.Request.Scheme = "none"
}
if builder.auditRequest.Request.Proto == "" {
builder.auditRequest.Request.Proto = "none"
}
if builder.auditRequest.Request.URL.Path == "" {
builder.auditRequest.Request.URL.Path = "none"
}
if builder.auditRequest.RequestClientIP == "" {
builder.auditRequest.RequestClientIP = quadZero
}
builder.WithEventType(EventTypeSystemEvent)
return builder
}
// WithRequiredApiRequest adds api request details
func (builder *AuditLogEntryBuilder) WithRequiredApiRequest(request ApiRequest) *AuditLogEntryBuilder {
builder.auditRequest.Request = &request
return builder
}
// WithRequiredLocation adds the region and optional zone id. If both, separated with a - (dash).
// Example: eu01
func (builder *AuditLogEntryBuilder) WithRequiredLocation(location string) *AuditLogEntryBuilder {
builder.location = location
return builder
}
// WithRequiredRequestClientIp adds the client ip
func (builder *AuditLogEntryBuilder) WithRequiredRequestClientIp(requestClientIp string) *AuditLogEntryBuilder {
builder.auditRequest.RequestClientIP = requestClientIp
return builder
}
// WithRequestCorrelationId adds an optional request correlation id
func (builder *AuditLogEntryBuilder) WithRequestCorrelationId(requestCorrelationId string) *AuditLogEntryBuilder {
builder.auditRequest.RequestCorrelationId = &requestCorrelationId
return builder
}
// WithRequestId adds an optional request id
func (builder *AuditLogEntryBuilder) WithRequestId(requestId string) *AuditLogEntryBuilder {
builder.auditRequest.RequestId = &requestId
return builder
}
// WithRequestTime sets the request time on the builder. If not set - the instantiation time of the builder is used.
func (builder *AuditLogEntryBuilder) WithRequestTime(requestTime time.Time) *AuditLogEntryBuilder {
builder.auditRequest.RequestTime = &requestTime
return builder
}
// WithRequiredServiceName adds the service name in lowercase (allowed characters are [a-z-]).
func (builder *AuditLogEntryBuilder) WithRequiredServiceName(serviceName string) *AuditLogEntryBuilder {
builder.auditMetadata.AuditServiceName = serviceName
return builder
}
// WithRequiredWorkerId adds the ID of the K8s Pod, Service-Instance, etc. (must be unique for a sending service)
func (builder *AuditLogEntryBuilder) WithRequiredWorkerId(workerId string) *AuditLogEntryBuilder {
builder.workerId = workerId
return builder
}
// WithRequiredObjectId adds the object identifier.
// May be prefilled by audit middleware (if the identifier can be extracted from the url path).
func (builder *AuditLogEntryBuilder) WithRequiredObjectId(objectId string) *AuditLogEntryBuilder {
builder.auditParams.ObjectId = objectId
return builder
}
// WithRequiredObjectType adds the object type.
// May be prefilled by audit middleware (if the type can be extracted from the url path).
func (builder *AuditLogEntryBuilder) WithRequiredObjectType(objectType ObjectType) *AuditLogEntryBuilder {
builder.auditParams.ObjectType = objectType
return builder
}
// WithRequiredOperation adds the name of the service method or operation.
//
// Format: stackit.<product>.<version>.<type-chain>.<operation>
// Where:
//
// Product: The name of the service in lowercase
// Version: Optional API version
// Type-Chain: Chained path to object
// Operation: The name of the operation in lowercase
//
// Examples:
//
// "stackit.resource-manager.v1.organizations.create"
// "stackit.authorization.v1.projects.volumes.create"
// "stackit.authorization.v2alpha.projects.volumes.create"
// "stackit.authorization.v2.folders.move"
// "stackit.resource-manager.health"
func (builder *AuditLogEntryBuilder) WithRequiredOperation(operation string) *AuditLogEntryBuilder {
builder.auditMetadata.AuditOperationName = operation
return builder
}
// WithAuditPermission adds the IAM permission
//
// Examples:
//
// "resourcemanager.project.edit"
func (builder *AuditLogEntryBuilder) WithAuditPermission(permission string) *AuditLogEntryBuilder {
builder.auditMetadata.AuditPermission = &permission
return builder
}
// WithAuditPermissionCheckResult adds the IAM permission check result
func (builder *AuditLogEntryBuilder) WithAuditPermissionCheckResult(permissionCheckResult bool) *AuditLogEntryBuilder {
builder.auditMetadata.AuditPermissionGranted = &permissionCheckResult
return builder
}
// WithLabels adds A set of user-defined (key, value) data that provides additional
// information about the log entry.
func (builder *AuditLogEntryBuilder) WithLabels(labels map[string]string) *AuditLogEntryBuilder {
builder.auditMetadata.AuditLabels = &labels
return builder
}
// WithNumResponseItems adds the number of items returned to the client if applicable.
func (builder *AuditLogEntryBuilder) WithNumResponseItems(numResponseItems int64) *AuditLogEntryBuilder {
builder.auditResponse.ResponseNumItems = &numResponseItems
return builder
}
// WithEventType overwrites the default event type EventTypeAdminActivity
func (builder *AuditLogEntryBuilder) WithEventType(eventType EventType) *AuditLogEntryBuilder {
builder.auditParams.EventType = eventType
return builder
}
// WithDetails adds an optional details object to the audit log entry
func (builder *AuditLogEntryBuilder) WithDetails(details map[string]interface{}) *AuditLogEntryBuilder {
builder.auditParams.Details = details
return builder
}
// WithSeverity overwrites the default log severity level auditV1.LogSeverity_LOG_SEVERITY_DEFAULT
func (builder *AuditLogEntryBuilder) WithSeverity(severity auditV1.LogSeverity) *AuditLogEntryBuilder {
builder.auditMetadata.AuditLogSeverity = severity
return builder
}
// WithStatusCode adds the (http) response status code
func (builder *AuditLogEntryBuilder) WithStatusCode(statusCode int) *AuditLogEntryBuilder {
builder.auditResponse.ResponseStatusCode = statusCode
return builder
}
// WithResponseBody adds the response body to the builder and transforms it in the Build method (json serializable or protobuf message expected)
func (builder *AuditLogEntryBuilder) WithResponseBody(responseBody any) *AuditLogEntryBuilder {
builder.auditParams.ResponseBody = responseBody
return builder
}
// WithResponseBodyBytes adds the response body as bytes (serialized json or protobuf message expected)
func (builder *AuditLogEntryBuilder) WithResponseBodyBytes(responseBody *[]byte) *AuditLogEntryBuilder {
builder.auditResponse.ResponseBodyBytes = responseBody
return builder
}
// WithResponseHeaders adds response headers
func (builder *AuditLogEntryBuilder) WithResponseHeaders(responseHeaders map[string][]string) *AuditLogEntryBuilder {
builder.auditResponse.ResponseHeaders = responseHeaders
return builder
}
// WithResponseTime adds the time when the response is sent
func (builder *AuditLogEntryBuilder) WithResponseTime(responseTime time.Time) *AuditLogEntryBuilder {
builder.auditResponse.ResponseTime = &responseTime
return builder
}
// Build constructs the auditV1.AuditLogEntry.
//
// Parameters:
// - A context object
// - A SequenceNumber
//
// Returns:
// - The auditV1.AuditLogEntry protobuf message or
// - Error if the entry cannot be built
func (builder *AuditLogEntryBuilder) Build(_ context.Context, sequenceNumber SequenceNumber) (*auditV1.AuditLogEntry, error) {
auditTime := time.Now()
builder.auditMetadata.AuditTime = &auditTime
objectId, objectType, err := getObjectIdAndTypeFromAuditParams(&builder.auditParams)
if err != nil {
return nil, err
}
if builder.auditResponse.ResponseBodyBytes != nil && builder.auditParams.ResponseBody != nil {
return nil, errors.New("responseBodyBytes and responseBody set")
} else if builder.auditParams.ResponseBody != nil {
responseBytes, err := ResponseBodyToBytes(builder.auditParams.ResponseBody)
if err != nil {
return nil, err
}
builder.auditResponse.ResponseBodyBytes = responseBytes
}
resourceName := fmt.Sprintf("%s/%s", objectType.Plural(), objectId)
var logIdentifier string
var logType ObjectType
if builder.auditParams.EventType == EventTypeSystemEvent {
logIdentifier = SystemIdentifier.Identifier
logType = ObjectTypeSystem
} else {
logIdentifier = objectId
logType = *objectType
}
builder.auditMetadata.AuditInsertId = NewInsertId(time.Now().UTC(), builder.location, builder.workerId, uint64(sequenceNumber))
builder.auditMetadata.AuditLogName = fmt.Sprintf("%s/%s/logs/%s", logType.Plural(), logIdentifier, builder.auditParams.EventType)
builder.auditMetadata.AuditResourceName = resourceName
var details *map[string]interface{} = nil
if len(builder.auditParams.Details) > 0 {
details = &builder.auditParams.Details
}
// Instantiate the audit event
return NewAuditLogEntry(
builder.auditRequest,
builder.auditResponse,
details,
builder.auditMetadata,
nil,
nil,
)
}
// AuditEventBuilder collects audit log parameters, validates input and
// returns a cloud event that can be sent to the audit log system.
type AuditEventBuilder struct {
// The audit api used to validate, serialize and send events
api *AuditApi
// The audit log entry builder which is used to build the actual protobuf message
auditLogEntryBuilder *AuditLogEntryBuilder
// Status whether the event has been built
built bool
// Sequence number generator providing sequential increasing numbers for the insert IDs
sequenceNumberGenerator *utils.SequenceNumberGenerator
// Opentelemetry tracer
tracer trace.Tracer
// Visibility of the event
visibility auditV1.Visibility
}
// NewAuditEventBuilder returns a builder that collects audit log parameters,
// validates input and returns a cloud event that can be sent to the audit log system.
func NewAuditEventBuilder(
// The audit api used to validate, serialize and send events
api *AuditApi,
// The sequence number generator can be used to get and revert sequence numbers to build audit log events
sequenceNumberGenerator *utils.SequenceNumberGenerator,
// Tracer
tracer trace.Tracer,
// The service name in lowercase (allowed characters are [a-z-]).
serviceName string,
// The ID of the K8s Pod, Service-Instance, etc. (must be unique for a sending service)
workerId string,
// The location of the service (e.g. eu01)
location string,
) *AuditEventBuilder {
return &AuditEventBuilder{
api: api,
auditLogEntryBuilder: NewAuditLogEntryBuilder().
WithRequiredServiceName(serviceName).
WithRequiredWorkerId(workerId).
WithRequiredLocation(location),
sequenceNumberGenerator: sequenceNumberGenerator,
tracer: tracer,
visibility: auditV1.Visibility_VISIBILITY_PUBLIC,
}
}
// NextSequenceNumber returns the next sequence number from utils.SequenceNumberGenerator.
// In case of an error RevertSequenceNumber must be called to prevent gaps in the sequence of numbers.
func (builder *AuditEventBuilder) NextSequenceNumber() SequenceNumber {
return SequenceNumber((*builder.sequenceNumberGenerator).Next())
}
// RevertSequenceNumber can be called to decrease the sequence number on the utils.SequenceNumberGenerator in case of an error
func (builder *AuditEventBuilder) RevertSequenceNumber() {
(*builder.sequenceNumberGenerator).Revert()
}
func (builder *AuditEventBuilder) AsSystemEvent() *AuditEventBuilder {
builder.auditLogEntryBuilder.AsSystemEvent()
builder.WithVisibility(auditV1.Visibility_VISIBILITY_PRIVATE)
return builder
}
// WithAuditLogEntryBuilder overwrites the preconfigured AuditLogEntryBuilder
func (builder *AuditEventBuilder) WithAuditLogEntryBuilder(auditLogEntryBuilder *AuditLogEntryBuilder) *AuditEventBuilder {
builder.auditLogEntryBuilder = auditLogEntryBuilder
return builder
}
// WithRequiredApiRequest adds api request details
func (builder *AuditEventBuilder) WithRequiredApiRequest(request ApiRequest) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequiredApiRequest(request)
return builder
}
// WithRequiredRequestClientIp adds the client ip
func (builder *AuditEventBuilder) WithRequiredRequestClientIp(requestClientIp string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequiredRequestClientIp(requestClientIp)
return builder
}
// WithRequestCorrelationId adds an optional request correlation id
func (builder *AuditEventBuilder) WithRequestCorrelationId(requestCorrelationId string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequestCorrelationId(requestCorrelationId)
return builder
}
// WithRequestId adds an optional request id
func (builder *AuditEventBuilder) WithRequestId(requestId string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequestId(requestId)
return builder
}
// WithRequestTime sets the request time on the builder. If not set - the instantiation time of the builder is used.
func (builder *AuditEventBuilder) WithRequestTime(requestTime time.Time) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequestTime(requestTime)
return builder
}
// WithRequiredObjectId adds the object identifier.
// May be prefilled by audit middleware (if the identifier can be extracted from the url path).
func (builder *AuditEventBuilder) WithRequiredObjectId(objectId string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequiredObjectId(objectId)
return builder
}
// WithRequiredObjectType adds the object type.
// May be prefilled by audit middleware (if the type can be extracted from the url path).
func (builder *AuditEventBuilder) WithRequiredObjectType(objectType ObjectType) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequiredObjectType(objectType)
return builder
}
// WithRequiredOperation adds the name of the service method or operation.
//
// Format: stackit.<product>.<version>.<type-chain>.<operation>
// Where:
//
// Product: The name of the service in lowercase
// Version: Optional API version
// Type-Chain: Chained path to object
// Operation: The name of the operation in lowercase
//
// Examples:
//
// "stackit.resource-manager.v1.organizations.create"
// "stackit.authorization.v1.projects.volumes.create"
// "stackit.authorization.v2alpha.projects.volumes.create"
// "stackit.authorization.v2.folders.move"
// "stackit.resource-manager.health"
func (builder *AuditEventBuilder) WithRequiredOperation(operation string) *AuditEventBuilder {
builder.auditLogEntryBuilder.auditMetadata.AuditOperationName = operation
return builder
}
// WithAuditPermission adds the IAM permission
//
// Examples:
//
// "resourcemanager.project.edit"
func (builder *AuditEventBuilder) WithAuditPermission(permission string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithAuditPermission(permission)
return builder
}
// WithAuditPermissionCheckResult adds the IAM permission check result
func (builder *AuditEventBuilder) WithAuditPermissionCheckResult(permissionCheckResult bool) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithAuditPermissionCheckResult(permissionCheckResult)
return builder
}
// WithLabels adds A set of user-defined (key, value) data that provides additional
// information about the log entry.
func (builder *AuditEventBuilder) WithLabels(labels map[string]string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithLabels(labels)
return builder
}
// WithNumResponseItems adds the number of items returned to the client if applicable.
func (builder *AuditEventBuilder) WithNumResponseItems(numResponseItems int64) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithNumResponseItems(numResponseItems)
return builder
}
// WithEventType overwrites the default event type EventTypeAdminActivity
func (builder *AuditEventBuilder) WithEventType(eventType EventType) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithEventType(eventType)
return builder
}
// WithDetails adds an optional details object to the audit log entry
func (builder *AuditEventBuilder) WithDetails(details map[string]interface{}) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithDetails(details)
return builder
}
// WithSeverity overwrites the default log severity level auditV1.LogSeverity_LOG_SEVERITY_DEFAULT
func (builder *AuditEventBuilder) WithSeverity(severity auditV1.LogSeverity) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithSeverity(severity)
return builder
}
// WithStatusCode adds the (http) response status code
func (builder *AuditEventBuilder) WithStatusCode(statusCode int) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithStatusCode(statusCode)
return builder
}
// WithResponseBody adds the response body to the builder and transforms it in the Build method (json serializable or protobuf message expected)
func (builder *AuditEventBuilder) WithResponseBody(responseBody any) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithResponseBody(responseBody)
return builder
}
// WithResponseBodyBytes adds the response body as bytes (serialized json or protobuf message expected)
func (builder *AuditEventBuilder) WithResponseBodyBytes(responseBody *[]byte) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithResponseBodyBytes(responseBody)
return builder
}
// WithResponseHeaders adds response headers
func (builder *AuditEventBuilder) WithResponseHeaders(responseHeaders map[string][]string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithResponseHeaders(responseHeaders)
return builder
}
// WithResponseTime adds the time when the response is sent
func (builder *AuditEventBuilder) WithResponseTime(responseTime time.Time) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithResponseTime(responseTime)
return builder
}
// WithVisibility overwrites the default visibility auditV1.Visibility_VISIBILITY_PUBLIC
func (builder *AuditEventBuilder) WithVisibility(visibility auditV1.Visibility) *AuditEventBuilder {
builder.visibility = visibility
return builder
}
// IsBuilt returns the status whether the cloud event has been built
func (builder *AuditEventBuilder) IsBuilt() bool {
return builder.built
}
// Build constructs the CloudEvent.
//
// Parameters:
// - A context object
// - A sequence number. AuditEventBuilder.NextSequenceNumber can be used to get the next SequenceNumber.
//
// Returns:
// - The CloudEvent containing the audit log entry
// - The RoutableIdentifier required for routing the cloud event
// - The operation name
// - Error if the event cannot be built
func (builder *AuditEventBuilder) Build(ctx context.Context, sequenceNumber SequenceNumber) (*CloudEvent, *RoutableIdentifier, error) {
if builder.auditLogEntryBuilder == nil {
return nil, nil, fmt.Errorf("audit log entry builder not set")
}
objectId := builder.auditLogEntryBuilder.auditParams.ObjectId
objectType := builder.auditLogEntryBuilder.auditParams.ObjectType
var routingIdentifier *RoutableIdentifier
if builder.auditLogEntryBuilder.auditParams.EventType == EventTypeSystemEvent {
routingIdentifier = NewAuditRoutingIdentifier(uuid.Nil.String(), ObjectTypeSystem)
if objectId == "" {
objectId = uuid.Nil.String()
builder.WithRequiredObjectId(objectId)
}
if objectType == "" {
objectType = ObjectTypeSystem
builder.WithRequiredObjectType(objectType)
}
} else {
routingIdentifier = NewAuditRoutingIdentifier(objectId, objectType)
}
auditLogEntry, err := builder.auditLogEntryBuilder.Build(ctx, sequenceNumber)
if err != nil {
return nil, nil, err
}
ctx, span := builder.tracer.Start(ctx, "create-audit-event")
defer span.End()
w3cTraceParent := TraceParentFromSpan(span)
var traceParent = &w3cTraceParent
var traceState *string = nil
visibility := builder.visibility
// Validate and serialize the protobuf event into a cloud event
_, validateSerializeSpan := builder.tracer.Start(ctx, "validate-and-serialize-audit-event")
cloudEvent, err := (*builder.api).ValidateAndSerializeWithTrace(auditLogEntry, visibility, routingIdentifier, traceParent, traceState)
validateSerializeSpan.End()
if err != nil {
return nil, nil, err
}
builder.built = true
return cloudEvent,
routingIdentifier,
nil
}

1167
audit/api/builder_test.go Normal file

File diff suppressed because it is too large Load diff

32
audit/api/converter.go Normal file
View file

@ -0,0 +1,32 @@
package api
import (
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
)
func StringToHttpMethod(method string) auditV1.AttributeContext_HttpMethod {
switch method {
case "GET":
return auditV1.AttributeContext_HTTP_METHOD_GET
case "HEAD":
return auditV1.AttributeContext_HTTP_METHOD_HEAD
case "POST":
return auditV1.AttributeContext_HTTP_METHOD_POST
case "PUT":
return auditV1.AttributeContext_HTTP_METHOD_PUT
case "DELETE":
return auditV1.AttributeContext_HTTP_METHOD_DELETE
case "CONNECT":
return auditV1.AttributeContext_HTTP_METHOD_CONNECT
case "OPTIONS":
return auditV1.AttributeContext_HTTP_METHOD_OPTIONS
case "TRACE":
return auditV1.AttributeContext_HTTP_METHOD_TRACE
case "PATCH":
return auditV1.AttributeContext_HTTP_METHOD_PATCH
case "OTHER":
return auditV1.AttributeContext_HTTP_METHOD_OTHER
default:
return auditV1.AttributeContext_HTTP_METHOD_UNSPECIFIED
}
}

100
audit/api/log.go Normal file
View file

@ -0,0 +1,100 @@
package api
import (
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/log"
"encoding/json"
"errors"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"time"
)
// LogEvent logs an event to the terminal
func LogEvent(event *CloudEvent) error {
if event.DataType == DataTypeLegacyAuditEventV1 {
log.AuditLogger.Info(string(event.Data))
return nil
} else if event.DataType != "audit.v1.RoutableAuditEvent" {
return errors.New("Unsupported data type " + event.DataType)
}
var routableAuditEvent auditV1.RoutableAuditEvent
err := proto.Unmarshal(event.Data, &routableAuditEvent)
if err != nil {
return err
}
var auditEvent auditV1.AuditLogEntry
err = proto.Unmarshal(routableAuditEvent.GetUnencryptedData().Data, &auditEvent)
if err != nil {
return err
}
// Convert to json
auditEventJson, err := protojson.Marshal(&auditEvent)
if err != nil {
return err
}
auditEventMap := make(map[string]interface{})
err = json.Unmarshal(auditEventJson, &auditEventMap)
if err != nil {
return err
}
objectIdentifierJson, err := protojson.Marshal(routableAuditEvent.ObjectIdentifier)
if err != nil {
return err
}
objectIdentifierMap := make(map[string]interface{})
err = json.Unmarshal(objectIdentifierJson, &objectIdentifierMap)
if err != nil {
return err
}
cloudEvent := cloudEvent{
SpecVersion: event.SpecVersion,
Source: event.Source,
Id: event.Id,
Time: event.Time,
DataContentType: event.DataContentType,
DataType: event.DataType,
Subject: event.Subject,
Data: routableEvent{
OperationName: auditEvent.ProtoPayload.OperationName,
Visibility: routableAuditEvent.Visibility.String(),
ResourceReference: objectIdentifierMap,
Data: auditEventMap,
},
TraceParent: event.TraceParent,
TraceState: event.TraceState,
}
cloudEventJson, err := json.Marshal(cloudEvent)
if err != nil {
return err
}
log.AuditLogger.Info(string(cloudEventJson))
return nil
}
type cloudEvent struct {
SpecVersion string
Source string
Id string
Time time.Time
DataContentType string
DataType string
Subject string
Data routableEvent
TraceParent *string
TraceState *string
}
type routableEvent struct {
OperationName string
Visibility string
ResourceReference map[string]interface{}
Data map[string]interface{}
}

101
audit/api/log_test.go Normal file
View file

@ -0,0 +1,101 @@
package api
import (
"context"
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/utils"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"github.com/bufbuild/protovalidate-go"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel"
"testing"
)
func Test_LogEvent(t *testing.T) {
api, _ := NewMockAuditApi()
sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator()
tracer := otel.Tracer("test-tracer")
t.Run("new format", func(t *testing.T) {
eventBuilder := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", uuid.NewString(), "eu01")
cloudEvent, _, err := eventBuilder.
WithRequiredApiRequest(ApiRequest{
Body: nil,
Header: TestHeaders,
Host: "localhost",
Method: "GET",
Scheme: "https",
Proto: "HTTP/1.1",
URL: RequestUrl{
Path: "/",
RawQuery: nil,
},
}).
WithRequiredObjectId(uuid.NewString()).
WithRequiredObjectType(ObjectTypeProject).
WithRequiredOperation("stackit.demo-service.v1.project.update").
WithRequiredRequestClientIp("0.0.0.0").
Build(context.Background(), eventBuilder.NextSequenceNumber())
assert.NoError(t, err)
assert.NoError(t, LogEvent(cloudEvent))
})
t.Run("legacy format", func(t *testing.T) {
objectId := uuid.NewString()
entry, err := NewAuditLogEntryBuilder().
WithRequiredApiRequest(ApiRequest{
Body: nil,
Header: TestHeaders,
Host: "localhost",
Method: "GET",
Scheme: "https",
Proto: "HTTP/1.1",
URL: RequestUrl{
Path: "/",
RawQuery: nil,
},
}).
WithRequiredLocation("eu01").
WithRequiredObjectId(objectId).
WithRequiredObjectType(ObjectTypeProject).
WithRequiredOperation("stackit.demo-service.v1.project.update").
WithRequiredRequestClientIp("0.0.0.0").
WithRequiredServiceName("demo-service").
WithRequiredWorkerId(uuid.NewString()).
Build(context.Background(), SequenceNumber(1))
assert.NoError(t, err)
validator, err := protovalidate.New()
assert.NoError(t, err)
var protoValidator ProtobufValidator = validator
routableIdentifier := RoutableIdentifier{
Identifier: objectId,
Type: ObjectTypeProject,
}
routableEvent, err := validateAndSerializePartially(&protoValidator, entry, auditV1.Visibility_VISIBILITY_PUBLIC, &routableIdentifier)
assert.NoError(t, err)
legacyBytes, err := convertAndSerializeIntoLegacyFormat(entry, routableEvent)
assert.NoError(t, err)
cloudEvent := CloudEvent{
SpecVersion: "1.0",
Source: entry.ProtoPayload.ServiceName,
Id: entry.InsertId,
Time: entry.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(),
DataContentType: ContentTypeCloudEventsJson,
DataType: DataTypeLegacyAuditEventV1,
Subject: entry.ProtoPayload.ResourceName,
Data: legacyBytes,
TraceParent: nil,
TraceState: nil,
}
assert.NoError(t, LogEvent(&cloudEvent))
})
}

983
audit/api/model.go Normal file
View file

@ -0,0 +1,983 @@
package api
import (
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/google/uuid"
"go.opentelemetry.io/otel/trace"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"google.golang.org/protobuf/types/known/wrapperspb"
"net"
"net/url"
"regexp"
"slices"
"strings"
"time"
)
var ErrInvalidRequestBody = errors.New("invalid request body")
var ErrInvalidResponse = errors.New("invalid response")
var ErrInvalidAuthorizationHeaderValue = errors.New("invalid authorization header value")
var ErrInvalidBearerToken = errors.New("invalid bearer token")
var ErrTokenIsNotBearerToken = errors.New("token is not a bearer token")
var objectTypeIdPattern, _ = regexp.Compile(".*/(projects|folders|organizations)/([0-9a-fA-F-]{36})(?:/.*)?")
type ApiRequest struct {
// Body
//
// Required: false
Body *[]byte
// The (HTTP) request headers / gRPC metadata.
//
// Internal IP-Addresses have to be removed (e.g. in x-forwarded-xxx headers).
//
// Required: true
Header map[string][]string
// The HTTP request `Host` header value.
//
// Required: true
Host string
// Method
//
// Required: true
Method string
// The URL scheme, such as `http`, `https` or `gRPC`.
//
// Required: true
Scheme string
// The network protocol used with the request, such as "http/1.1",
// "spdy/3", "h2", "h2c", "webrtc", "tcp", "udp", "quic". See
// https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
// for details.
//
// Required: true
Proto string
// The url
//
// Required: true
URL RequestUrl
}
type RequestUrl struct {
// The gRPC / HTTP URL path.
//
// Required: true
Path string
// The HTTP URL query in the format of "name1=value1&name2=value2", as it
// appears in the first line of the HTTP request.
// The input should be escaped to not contain any special characters.
//
// Required: false
RawQuery *string
}
// AuditRequest bundles request related parameters
type AuditRequest struct {
// The operation request. This may not include all request parameters,
// such as those that are too large, privacy-sensitive, or duplicated
// elsewhere in the log record.
// It should never include user-generated data, such as file contents.
//
// Required: true
Request *ApiRequest
// The IP address of the caller.
// For caller from internet, this will be public IPv4 or IPv6 address.
// For caller from a VM / K8s Service / etc., this will be the SIT proxy's IPv4 address.
//
// Required: true
RequestClientIP string
// Correlate multiple audit logs by setting the same id
//
// Required: false
RequestCorrelationId *string
// The unique ID for a request, which can be propagated to downstream
// systems. The ID should have low probability of collision
// within a single day for a specific service.
//
// More information can be found here: https://google.aip.dev/155
//
// Format: <idempotency-key>
// Where:
// Idempotency-key: Typically consists of an id + version
//
// Examples:
// 5e3952a9-b628-4be6-ac61-b1c6eb4a110c/5
//
// Required: false
RequestId *string
// The timestamp when the `destination` service receives the first byte of
// the request.
//
// Required: false
RequestTime *time.Time
}
// AuditResponse bundles response related parameters
type AuditResponse struct {
// The operation response. This may not include all response elements,
// such as those that are too large, privacy-sensitive, or duplicated
// elsewhere in the log record.
//
// Required: false
ResponseBodyBytes *[]byte
// The http or gRPC status code.
//
// Examples:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
// https://grpc.github.io/grpc/core/md_doc_statuscodes.html
//
// Required: true
ResponseStatusCode int
// The HTTP response headers.
//
// Required: true
ResponseHeaders map[string][]string
// The number of items returned from a List or Query API method,
// if applicable.
//
// Required: false
ResponseNumItems *int64
// The timestamp when the "destination" service generates the first byte of
// the response.
//
// Required: false
ResponseTime *time.Time
}
// AuditMetadata bundles audit event related metadata
type AuditMetadata struct {
// A unique identifier for the log entry.
// Is used to check completeness of audit events over time.
//
// Format: <unix-timestamp>/<region-zone>/<worker-id>/<sequence-number>
// Where:
// Unix-Timestamp: A UTC unix timestamp in seconds is expected
// Region-Zone: The region and (optional) zone id. If both, separated with a - (dash)
// Worker-Id: The ID of the K8s Pod, Service-Instance, etc. (must be unique for a sending service)
// Sequence-Number: Increasing number, representing the message offset per Worker-Id
// If the Worker-Id changes, the sequence-number has to be reset to 0.
//
// Examples:
// "1721899117/eu01/319a7fb9-edd2-46c6-953a-a724bb377c61/8792726390909855142"
//
// Required: true
AuditInsertId string
// A set of user-defined (key, value) data that provides additional
// information about the log entry.
//
// Required: false
AuditLabels *map[string]string
// The resource name of the log to which this log entry belongs.
//
// Format: <pluralType>/<identifier>/logs/<eventType>
// Where:
// Plural-Types: One from the list of supported ObjectType as plural
// Event-Types: admin-activity, system-event, policy-denied, data-access
//
// Examples:
// "projects/00b0f972-59ff-48f2-a4f9-29c57b75c2fa/logs/admin-activity"
//
// Required: true
AuditLogName string
// The severity of the log entry.
//
// Required: true
AuditLogSeverity auditV1.LogSeverity
// The name of the service method or operation.
//
// Format: stackit.<product>.<version>.<type-chain>.<operation>
// Where:
// Product: The name of the service in lowercase
// Version: Optional API version
// Type-Chain: Optional chained path to object
// Operation: The name of the operation in lowercase
//
// Examples:
// "stackit.resource-manager.v1.organizations.create"
// "stackit.authorization.v1.projects.volumes.create"
// "stackit.authorization.v2alpha.projects.volumes.create"
// "stackit.authorization.v2.folders.move"
// "stackit.resource-manager.health"
//
// Required: true
AuditOperationName string
// The required IAM permission.
//
// Examples:
// "resourcemanager.project.edit"
//
// Required: false
AuditPermission *string
// Result of the IAM permission check.
//
// Required: false
AuditPermissionGranted *bool
// The resource or collection that is the target of the operation.
// The name is a scheme-less URI, not including the API service name.
//
// Format: <pluralType>/<id>[/locations/<region-zone>][/<details>]
// Where:
// Plural-Type: One from the list of supported ObjectType as plural
// Id: The identifier of the object
// Region-Zone: Optional region and zone id. If both, separated with a - (dash). Alternatively _ (underscore).
// Details: Optional "<key>/<id>" pairs
//
// Examples:
// "organizations/40ab14ad-b7b0-4b1c-be41-5bc820a968d1"
// "projects/7046e7b6-5ae9-441c-99fe-2cd28a5078ec/locations/_/instances/instance-20240723-174217"
// "projects/7046e7b6-5ae9-441c-99fe-2cd28a5078ec/locations/eu01/instances/instance-20240723-174217"
// "projects/7046e7b6-5ae9-441c-99fe-2cd28a5078ec/locations/sx-stoi01/instances/instance-20240723-174217"
// "projects/dd7d1807-54e9-4426-8994-721758b5b554/locations/eu01-m/vms/b6851b4e-7a9d-4973-ab0f-a80a13ee3060/ports/78f8bad4-a291-4fa3-b07f-4a1985d3dbe8"
//
// Required: true
AuditResourceName string
// The name of the API service performing the operation.
//
// Examples:
// "resource-manager"
//
// Required: true
AuditServiceName string
// The time the event described by the log entry occurred.
//
// Required: false
AuditTime *time.Time
}
// NewAuditLogEntry constructs a new audit log event for the given parameters
func NewAuditLogEntry(
// Required request parameters
auditRequest AuditRequest,
// Required response parameters
auditResponse AuditResponse,
// Optional map that is added as "details" to the message
eventMetadata *map[string]interface{},
// Required metadata
auditMetadata AuditMetadata,
// Optional W3C trace parent
userProvidedTraceParent *string,
// Optional W3C trace state
userProvidedTraceState *string,
) (*auditV1.AuditLogEntry, error) {
// Get request headers
filteredRequestHeaders := FilterAndMergeHeaders(auditRequest.Request.Header)
filteredResponseHeaders := FilterAndMergeHeaders(auditResponse.ResponseHeaders)
// Get response body
responseBody, err := NewResponseBody(auditResponse.ResponseBodyBytes)
if err != nil {
return nil, errors.Join(err, ErrInvalidResponse)
}
var responseLength *int64 = nil
if responseBody != nil {
length := int64(len(*auditResponse.ResponseBodyBytes))
responseLength = &length
}
// Get request body
requestBody, err := NewRequestBody(auditRequest.Request)
if err != nil {
return nil, errors.Join(err, ErrInvalidRequestBody)
}
// Get audit attributes from request
auditClaims, authenticationPrincipal, audiences, authenticationInfo, err :=
AuditAttributesFromAuthorizationHeader(auditRequest.Request)
if err != nil {
return nil, err
}
// Get request scheme (http, https)
scheme := auditRequest.Request.Scheme
// Initialize authorization info if available
var authorizationInfo []*auditV1.AuthorizationInfo = nil
if auditMetadata.AuditPermission != nil && auditMetadata.AuditPermissionGranted != nil {
authorizationInfo = []*auditV1.AuthorizationInfo{
NewAuthorizationInfo(
auditMetadata.AuditResourceName,
*auditMetadata.AuditPermission,
*auditMetadata.AuditPermissionGranted)}
}
// Initialize labels if available
var labels map[string]string = nil
if auditMetadata.AuditLabels != nil {
labels = *auditMetadata.AuditLabels
}
// Initialize metadata/details
var metadata *structpb.Struct = nil
if eventMetadata != nil {
metadataStruct, err := structpb.NewStruct(*eventMetadata)
if err != nil {
return nil, err
}
metadata = metadataStruct
}
// Get request and audit time
var concreteRequestTime = time.Now().UTC()
if auditRequest.RequestTime != nil {
concreteRequestTime = *auditRequest.RequestTime
}
var concreteAuditTime = concreteRequestTime
if auditMetadata.AuditTime != nil {
concreteAuditTime = *auditMetadata.AuditTime
}
var concreteResponseTime = concreteRequestTime
if auditResponse.ResponseTime != nil {
concreteResponseTime = *auditResponse.ResponseTime
}
// Initialize the audit log entry
event := auditV1.AuditLogEntry{
LogName: auditMetadata.AuditLogName,
ProtoPayload: &auditV1.AuditLog{
ServiceName: auditMetadata.AuditServiceName,
OperationName: auditMetadata.AuditOperationName,
ResourceName: auditMetadata.AuditResourceName,
AuthenticationInfo: authenticationInfo,
AuthorizationInfo: authorizationInfo,
RequestMetadata: NewRequestMetadata(
auditRequest.Request,
filteredRequestHeaders,
auditRequest.RequestId,
scheme,
concreteRequestTime,
auditRequest.RequestClientIP,
authenticationPrincipal,
audiences,
auditClaims),
Request: requestBody,
ResponseMetadata: NewResponseMetadata(
auditResponse.ResponseStatusCode,
auditResponse.ResponseNumItems,
responseLength,
filteredResponseHeaders,
concreteResponseTime),
Response: responseBody,
Metadata: metadata,
},
InsertId: auditMetadata.AuditInsertId,
Labels: labels,
CorrelationId: auditRequest.RequestCorrelationId,
Timestamp: timestamppb.New(concreteAuditTime),
Severity: auditMetadata.AuditLogSeverity,
TraceParent: userProvidedTraceParent,
TraceState: userProvidedTraceState,
}
return &event, nil
}
// GetCalledServiceNameFromRequest extracts the called service name from subdomain name
func GetCalledServiceNameFromRequest(request *ApiRequest, fallbackName string) string {
if request == nil {
return fallbackName
}
var calledServiceName = fallbackName
host := request.Host
ip := net.ParseIP(host)
if ip == nil && !strings.Contains(host, "localhost") {
dotIdx := strings.Index(host, ".")
if dotIdx != -1 {
calledServiceName = host[0:dotIdx]
}
}
return calledServiceName
}
// AuditSpan is an abstraction for trace.Span that can easier be tested
type AuditSpan interface {
SpanContext() trace.SpanContext
}
// TraceParentFromSpan returns W3C conform trace parent from AuditSpan
func TraceParentFromSpan(span AuditSpan) string {
traceVersion := "00"
traceId := span.SpanContext().TraceID().String()
parentId := span.SpanContext().SpanID().String()
// Trace flags according to W3C documentation:
// https://www.w3.org/TR/trace-context/#sampled-flag
var traceFlags = "00"
if span.SpanContext().TraceFlags().IsSampled() {
traceFlags = "01"
}
// Format: <version>-<trace-id>-<parent-id>-<trace-flags>
// Example: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
w3cTraceParent := fmt.Sprintf("%s-%s-%s-%s", traceVersion, traceId, parentId, traceFlags)
return w3cTraceParent
}
// NewPbInt64Value returns protobuf int64 wrapper if value is not nil.
func NewPbInt64Value(value *int64) *wrapperspb.Int64Value {
if value != nil {
return wrapperspb.Int64(*value)
}
return nil
}
// NewRequestMetadata returns initialized protobuf RequestMetadata object.
func NewRequestMetadata(
request *ApiRequest,
requestHeaders map[string]string,
requestId *string,
requestScheme string,
requestTime time.Time,
clientIp string,
authenticationPrincipal string,
audiences []string,
auditClaims *structpb.Struct,
) *auditV1.RequestMetadata {
agent := requestHeaders["User-Agent"]
if agent == "" {
agent = requestHeaders["user-agent"]
}
return &auditV1.RequestMetadata{
CallerIp: clientIp,
CallerSuppliedUserAgent: agent,
RequestAttributes: NewRequestAttributes(
request,
requestHeaders,
requestId,
requestScheme,
requestTime,
authenticationPrincipal,
audiences,
auditClaims,
),
}
}
// NewRequestAttributes returns initialized protobuf AttributeContext_Request object.
func NewRequestAttributes(
request *ApiRequest,
requestHeaders map[string]string,
requestId *string,
requestScheme string,
requestTime time.Time,
authenticationPrincipal string,
audiences []string,
auditClaims *structpb.Struct,
) *auditV1.AttributeContext_Request {
rawQuery := request.URL.RawQuery
var query *string = nil
if rawQuery != nil && *rawQuery != "" {
escapedQuery := url.QueryEscape(*rawQuery)
query = &escapedQuery
}
return &auditV1.AttributeContext_Request{
Id: requestId,
Method: StringToHttpMethod(request.Method),
Headers: requestHeaders,
Path: request.URL.Path,
Host: request.Host,
Scheme: requestScheme,
Query: query,
Time: timestamppb.New(requestTime),
Protocol: request.Proto,
Auth: &auditV1.AttributeContext_Auth{
Principal: authenticationPrincipal,
Audiences: audiences,
Claims: auditClaims,
},
}
}
// NewAuthorizationInfo returns protobuf AuthorizationInfo for the given parameters.
func NewAuthorizationInfo(resourceName string, permission string, granted bool) *auditV1.AuthorizationInfo {
return &auditV1.AuthorizationInfo{
Resource: resourceName,
Permission: &permission,
Granted: &granted,
}
}
// NewInsertId returns a correctly formatted insert id.
func NewInsertId(insertTime time.Time, location string, workerId string, eventSequenceNumber uint64) string {
return fmt.Sprintf("%d/%s/%s/%d", insertTime.UnixNano(), location, workerId, eventSequenceNumber)
}
// NewResponseMetadata returns protobuf response status with status code and short message.
func NewResponseMetadata(statusCode int, numResponseItems *int64, responseSize *int64, headers map[string]string, responseTime time.Time) *auditV1.ResponseMetadata {
var message *string = nil
if statusCode >= 400 && statusCode < 500 {
text := "Client error"
message = &text
} else if statusCode >= 500 {
text := "Server error"
message = &text
}
var size *wrapperspb.Int64Value = nil
if responseSize != nil {
size = wrapperspb.Int64(*responseSize)
}
return &auditV1.ResponseMetadata{
StatusCode: wrapperspb.Int32(int32(statusCode)),
ErrorMessage: message,
ErrorDetails: nil,
ResponseAttributes: &auditV1.AttributeContext_Response{
NumResponseItems: NewPbInt64Value(numResponseItems),
Size: size,
Headers: headers,
Time: timestamppb.New(responseTime),
},
}
}
// NewResponseBody converts the JSON byte response into a protobuf struct.
func NewResponseBody(response *[]byte) (*structpb.Struct, error) {
// Return if nil
if response == nil || len(*response) == 0 {
return nil, nil
}
// Convert to protobuf struct
return byteArrayToPbStruct(*response)
}
// NewRequestBody converts the request body into a protobuf struct.
func NewRequestBody(request *ApiRequest) (*structpb.Struct, error) {
if request.Body == nil || len(*request.Body) == 0 {
return nil, nil
}
// Convert to protobuf struct
return byteArrayToPbStruct(*request.Body)
}
// byteArrayToPbStruct converts a given json byte array into a protobuf struct.
func byteArrayToPbStruct(bytes []byte) (*structpb.Struct, error) {
var bodyMap map[string]interface{}
err := json.Unmarshal(bytes, &bodyMap)
if err != nil {
return nil, err
}
return structpb.NewStruct(bodyMap)
}
// FilterAndMergeHeaders filters ":authority", "Authorization" and "B3" headers as well as
// all headers starting with the prefixes "X-" and "STACKIT-".
// Headers are merged if there is more than one value for a given name.
func FilterAndMergeHeaders(headers map[string][]string) map[string]string {
var resultMap = make(map[string]string)
skipHeaders := []string{":authority", "authorization", "b3"}
skipPrefixHeaders := []string{"x-", "stackit-"}
if len(headers) == 0 {
return nil
}
for headerName, headerValues := range headers {
headerLower := strings.ToLower(headerName)
// Check if headers with a specific prefix is found
skip := false
for _, skipPrefix := range skipPrefixHeaders {
if strings.HasPrefix(headerLower, skipPrefix) {
skip = true
break
}
}
// Keep header if not on filter list or value is empty
if !skip && !slices.Contains(skipHeaders, headerLower) && len(headerValues) > 0 {
resultMap[headerName] = strings.Join(headerValues, ",")
}
}
return resultMap
}
// NewAuditRoutingIdentifier instantiates a new auditApi.RoutableIdentifier for
// the given object ID and object type.
func NewAuditRoutingIdentifier(objectId string, objectType ObjectType) *RoutableIdentifier {
return &RoutableIdentifier{
Identifier: objectId,
Type: objectType,
}
}
// AuditAttributesFromAuthorizationHeader extracts the following claims from given http.Request:
// - auditClaims - filtered list of claims
// - authenticationPrincipal - principal identifier
// - audiences - list of audience claims
// - authenticationInfo - information about the user or service-account authentication
func AuditAttributesFromAuthorizationHeader(request *ApiRequest) (
*structpb.Struct,
string,
[]string,
*auditV1.AuthenticationInfo,
error,
) {
var principalId = "none"
var principalEmail = "do-not-reply@stackit.cloud"
emptyClaims, _ := structpb.NewStruct(make(map[string]interface{}))
var auditClaims = emptyClaims
var authenticationPrincipal = "none/none"
var serviceAccountName *string = nil
audiences := make([]string, 0)
var delegationInfo []*auditV1.ServiceAccountDelegationInfo = nil
authorizationHeaders := request.Header["Authorization"]
if len(authorizationHeaders) == 0 {
// fallback for grpc where headers/metadata keys are lowercase
authorizationHeaders = request.Header["authorization"]
}
authorizationHeader := strings.Join(authorizationHeaders, ",")
trimmedAuthorizationHeader := strings.TrimSpace(authorizationHeader)
if len(trimmedAuthorizationHeader) > 0 {
// Parse claims
parsedClaims, filteredClaims, err := parseClaimsFromAuthorizationHeader(trimmedAuthorizationHeader)
if err != nil {
return nil, authenticationPrincipal, nil, nil, err
}
// Convert filtered claims to protobuf struct
auditClaimsStruct, err := structpb.NewStruct(filteredClaims)
if err != nil {
return nil, authenticationPrincipal, nil, nil, err
}
auditClaims = auditClaimsStruct
// Extract principal data
authenticationPrincipal = extractAuthenticationPrincipal(parsedClaims)
principalId, principalEmail = extractSubjectAndEmail(parsedClaims)
// Extract service account delegation info data
delegationInfo = extractServiceAccountDelegationInfo(parsedClaims)
// Extract audiences data
audiences, err = extractAudiences(parsedClaims)
if err != nil {
return nil, authenticationPrincipal, nil, nil, err
}
// Extract project id and service account id
projectId := extractServiceAccountProjectId(parsedClaims)
serviceAccountId := extractServiceAccountId(parsedClaims)
// Calculate service account name if project id and service account id are available
if projectId != nil && serviceAccountId != nil {
accountName := fmt.Sprintf("projects/%s/service-accounts/%s", *projectId, *serviceAccountId)
serviceAccountName = &accountName
}
}
authenticationInfo := auditV1.AuthenticationInfo{
PrincipalId: principalId,
PrincipalEmail: principalEmail,
ServiceAccountName: serviceAccountName,
ServiceAccountDelegationInfo: delegationInfo,
}
return auditClaims, authenticationPrincipal, audiences, &authenticationInfo, nil
}
func extractServiceAccountId(parsedClaims map[string]interface{}) *string {
projectId, projectIdExists := parsedClaims["stackit/serviceaccount/service-account.uid"]
if projectIdExists {
projectIdString := fmt.Sprintf("%s", projectId)
return &projectIdString
} else {
return nil
}
}
func extractServiceAccountProjectId(parsedClaims map[string]interface{}) *string {
projectId, projectIdExists := parsedClaims["stackit/project/project.id"]
if projectIdExists {
projectIdString := fmt.Sprintf("%s", projectId)
return &projectIdString
} else {
return nil
}
}
func extractAudiences(parsedClaims map[string]interface{}) ([]string, error) {
audClaim, _ := json.Marshal(parsedClaims["aud"])
var audiences []string
if err := json.Unmarshal(audClaim, &audiences); err != nil {
return nil, err
}
return audiences, nil
}
func extractAuthenticationPrincipal(parsedClaims map[string]interface{}) string {
subClaim, subExists := parsedClaims["sub"]
issuerClaim, issuerExists := parsedClaims["iss"]
var principal = "none/none"
if subExists && issuerExists {
principal = fmt.Sprintf("%s/%s", url.QueryEscape(subClaim.(string)), url.QueryEscape(issuerClaim.(string)))
}
return principal
}
func parseClaimsFromAuthorizationHeader(authorizationHeader string) (map[string]interface{}, map[string]interface{}, error) {
parts := strings.Split(authorizationHeader, " ")
if len(parts) != 2 {
return nil, nil, ErrInvalidAuthorizationHeaderValue
}
if !strings.EqualFold(parts[0], "Bearer") {
return nil, nil, ErrTokenIsNotBearerToken
}
jwt := parts[1]
authorizationHeaderParts := strings.Split(jwt, ".")
parsedClaims := make(map[string]interface{})
if len(authorizationHeaderParts) == 3 {
// base64 decoding
decodedString, err := base64.RawURLEncoding.DecodeString(authorizationHeaderParts[1])
if err != nil {
return parsedClaims, nil, errors.Join(err, ErrInvalidBearerToken)
}
// unmarshall claim part of token
err = json.Unmarshal(decodedString, &parsedClaims)
if err != nil {
return parsedClaims, nil, err
}
// Collect user-friendly filtered subset of claims
filteredClaims := make(map[string]interface{})
_ = json.Unmarshal(decodedString, &filteredClaims)
keysToDelete := make([]string, 0)
for key := range filteredClaims {
if key != "aud" && key != "email" && key != "iss" && key != "jti" && key != "sub" {
keysToDelete = append(keysToDelete, key)
}
}
for _, key := range keysToDelete {
delete(filteredClaims, key)
}
return parsedClaims, filteredClaims, nil
}
return parsedClaims, nil, ErrInvalidBearerToken
}
func extractServiceAccountDelegationInfoDetails(token map[string]interface{}) []*auditV1.ServiceAccountDelegationInfo {
principalId, principalEmail := extractSubjectAndEmail(token)
delegation := auditV1.ServiceAccountDelegationInfo{Authority: &auditV1.ServiceAccountDelegationInfo_IdpPrincipal_{IdpPrincipal: &auditV1.ServiceAccountDelegationInfo_IdpPrincipal{
PrincipalId: principalId,
PrincipalEmail: principalEmail,
ServiceMetadata: nil,
}}}
delegations := []*auditV1.ServiceAccountDelegationInfo{&delegation}
nestedDelegations := extractServiceAccountDelegationInfo(token)
if len(nestedDelegations) > 0 {
return append(delegations, nestedDelegations...)
} else {
return delegations
}
}
func extractServiceAccountDelegationInfo(token map[string]interface{}) []*auditV1.ServiceAccountDelegationInfo {
actor := token["act"]
if actor != nil {
actorMap, hasActorClaim := actor.(map[string]interface{})
if hasActorClaim {
return extractServiceAccountDelegationInfoDetails(actorMap)
}
}
return nil
}
func extractSubjectAndEmail(token map[string]interface{}) (string, string) {
var principalEmail string
principalId := fmt.Sprintf("%s", token["sub"])
principalEmailRaw := token["email"]
if principalEmailRaw == nil {
principalEmail = "do-not-reply@stackit.cloud"
} else {
principalEmail = fmt.Sprintf("%s", principalEmailRaw)
}
return principalId, principalEmail
}
// OperationNameFromUrlPath converts the request url path into an operation name.
// UUIDs and query parameters are filtered out, slashes replaced by dots.
// HTTP methods are added as suffix as follows:
// - POST - create
// - PUT - update
// - PATCH - update
// - DELETE - delete
// - others - read
func OperationNameFromUrlPath(path string, requestMethod string) string {
queryIdx := strings.Index(path, "?")
if queryIdx != -1 {
path = path[:queryIdx]
}
path = strings.TrimPrefix(path, "/")
path = strings.TrimSuffix(path, "/")
split := strings.Split(path, "/")
operation := ""
for _, part := range split {
// skip uuids in path
_, err := uuid.Parse(part)
if err == nil {
continue
}
operation = fmt.Sprintf("%s/%s", operation, part)
}
operation = strings.ReplaceAll(operation, "/", ".")
operation = strings.TrimPrefix(operation, ".")
operation = strings.ToLower(operation)
if len(operation) > 0 {
method := StringToHttpMethod(requestMethod)
var action string
switch method {
case auditV1.AttributeContext_HTTP_METHOD_PUT:
fallthrough
case auditV1.AttributeContext_HTTP_METHOD_PATCH:
action = "update"
case auditV1.AttributeContext_HTTP_METHOD_POST:
action = "create"
case auditV1.AttributeContext_HTTP_METHOD_DELETE:
action = "delete"
default:
action = "read"
}
operation = fmt.Sprintf("%s.%s", operation, action)
}
return operation
}
// OperationNameFromGrpcMethod converts the grpc path into an operation name.
func OperationNameFromGrpcMethod(path string) string {
operation := strings.TrimPrefix(path, "/")
operation = strings.TrimSuffix(operation, "/")
operation = strings.ReplaceAll(operation, "/", ".")
operation = strings.TrimPrefix(operation, ".")
operation = strings.ToLower(operation)
return operation
}
func GetObjectIdAndTypeFromUrlPath(path string) (
string,
*ObjectType,
error,
) {
// Extract object id and type from request url
objectTypeIdMatches := objectTypeIdPattern.FindStringSubmatch(path)
if len(objectTypeIdMatches) > 0 {
objectType := ObjectTypeFromPluralString(objectTypeIdMatches[1])
err := objectType.IsSupportedType()
if err != nil {
return "", nil, err
}
objectId := objectTypeIdMatches[2]
return objectId, &objectType, nil
}
return "", nil, nil
}
func ToArrayMap(input map[string]string) map[string][]string {
output := map[string][]string{}
for key, value := range input {
output[key] = []string{value}
}
return output
}
func StringAttributeFromMetadata(metadata map[string][]string, name string) string {
var value = ""
rawValue, hasAttribute := metadata[name]
if hasAttribute && len(rawValue) > 0 {
value = rawValue[0]
}
return value
}
// ResponseBodyToBytes converts a JSON or Protobuf response into a byte array
func ResponseBodyToBytes(response any) (*[]byte, error) {
if response == nil {
return nil, nil
}
responseBytes, isBytes := response.([]byte)
if isBytes {
return &responseBytes, nil
}
responseProtoMessage, isProtoMessage := response.(proto.Message)
if isProtoMessage {
responseJson, err := protojson.Marshal(responseProtoMessage)
if err != nil {
return nil, err
}
return &responseJson, nil
} else {
responseJson, err := json.Marshal(response)
if err != nil {
return nil, err
}
return &responseJson, nil
}
}

1121
audit/api/model_test.go Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,128 @@
package api
import (
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"github.com/bufbuild/protovalidate-go"
"github.com/stretchr/testify/assert"
"testing"
)
func Test_RoutableAuditEvent(t *testing.T) {
validator, err := protovalidate.New()
assert.NoError(t, err)
newEvent := func() auditV1.RoutableAuditEvent {
return auditV1.RoutableAuditEvent{
OperationName: "stackit.resource-manager.v1.organizations.create",
Visibility: auditV1.Visibility_VISIBILITY_PUBLIC,
ObjectIdentifier: &auditV1.ObjectIdentifier{
Identifier: "14f7aa86-77ba-4d77-a091-a2cf3395a221",
Type: string(ObjectTypeProject),
},
Data: &auditV1.RoutableAuditEvent_UnencryptedData{UnencryptedData: &auditV1.UnencryptedData{
Data: []byte("data"),
ProtobufType: "audit.v1.AuditLogEntry",
}}}
}
t.Run("valid event", func(t *testing.T) {
event := newEvent()
err := validator.Validate(&event)
assert.NoError(t, err)
})
t.Run("empty operation name", func(t *testing.T) {
event := newEvent()
event.OperationName = ""
err := validator.Validate(&event)
assert.EqualError(t, err, "validation error:\n - operation_name: value is required [required]")
})
t.Run("invalid operation name", func(t *testing.T) {
event := newEvent()
event.OperationName = "stackit.resource-manager.v1.INVALID.organizations.create"
err := validator.Validate(&event)
assert.EqualError(t, err, "validation error:\n - operation_name: value does not match regex pattern `^stackit\\.[a-z0-9-]+\\.(?:v[0-9]+\\.)?(?:[a-z0-9-.]+\\.)?[a-z0-9-]+$` [string.pattern]")
})
t.Run("visibility invalid", func(t *testing.T) {
event := newEvent()
event.Visibility = -1
err := validator.Validate(&event)
assert.EqualError(t, err, "validation error:\n - visibility: value must be one of the defined enum values [enum.defined_only]")
})
t.Run("visibility unspecified", func(t *testing.T) {
event := newEvent()
event.Visibility = auditV1.Visibility_VISIBILITY_UNSPECIFIED
err := validator.Validate(&event)
assert.EqualError(t, err, "validation error:\n - visibility: value is required [required]")
})
t.Run("object identifier nil", func(t *testing.T) {
event := newEvent()
event.ObjectIdentifier = nil
err := validator.Validate(&event)
assert.EqualError(t, err, "validation error:\n - object_identifier: value is required [required]")
})
t.Run("object identifier id empty", func(t *testing.T) {
event := newEvent()
event.ObjectIdentifier.Identifier = ""
err := validator.Validate(&event)
assert.EqualError(t, err, "validation error:\n - object_identifier.identifier: value is required [required]")
})
t.Run("object identifier id not uuid", func(t *testing.T) {
event := newEvent()
event.ObjectIdentifier.Identifier = "invalid"
err := validator.Validate(&event)
assert.EqualError(t, err, "validation error:\n - object_identifier.identifier: value must be a valid UUID [string.uuid]")
})
t.Run("object identifier type empty", func(t *testing.T) {
event := newEvent()
event.ObjectIdentifier.Type = ""
err := validator.Validate(&event)
assert.EqualError(t, err, "validation error:\n - object_identifier.type: value is required [required]")
})
t.Run("data nil", func(t *testing.T) {
event := newEvent()
event.Data = nil
err := validator.Validate(&event)
assert.EqualError(t, err, "validation error:\n - data: exactly one field is required in oneof [required]")
})
t.Run("data empty", func(t *testing.T) {
event := newEvent()
event.Data = &auditV1.RoutableAuditEvent_UnencryptedData{UnencryptedData: &auditV1.UnencryptedData{
Data: []byte{},
ProtobufType: "audit.v1.AuditLogEntry",
}}
err := validator.Validate(&event)
assert.EqualError(t, err, "validation error:\n - unencrypted_data.data: value is required [required]")
})
t.Run("data protobuf type empty", func(t *testing.T) {
event := newEvent()
event.Data = &auditV1.RoutableAuditEvent_UnencryptedData{UnencryptedData: &auditV1.UnencryptedData{
Data: []byte("data"),
ProtobufType: "",
}}
err := validator.Validate(&event)
assert.EqualError(t, err, "validation error:\n - unencrypted_data.protobuf_type: value is required [required]")
})
}

462
audit/api/test_data.go Normal file
View file

@ -0,0 +1,462 @@
package api
import (
"fmt"
"time"
"google.golang.org/protobuf/types/known/wrapperspb"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"github.com/google/uuid"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
)
const clientCredentialsToken = "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOGJlZjc1LWRmY2QtNGE3My1hMzkxLTU0YTdhZjU3YTdkNiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic3RhY2tpdC1yZXNvdXJjZS1tYW5hZ2VyLWRldiJdLCJjbGllbnRfaWQiOiJzdGFja2l0LXJlc291cmNlLW1hbmFnZXItZGV2IiwiZXhwIjoxNzI0NDA1MzI2LCJpYXQiOjE3MjQ0MDQ0MjYsImlzcyI6Imh0dHBzOi8vYWNjb3VudHMuZGV2LnN0YWNraXQuY2xvdWQiLCJqdGkiOiJlNDZlYmEzOC1kZWRiLTQ1NDEtOTRmMy00OWY5N2E5MzRkNTgiLCJuYmYiOjE3MjQ0MDQ0MjYsInNjb3BlIjoidWFhLm5vbmUiLCJzdWIiOiJzdGFja2l0LXJlc291cmNlLW1hbmFnZXItZGV2In0.JP5Uy7AMdK4ukzQ6aOYzbVwEmq0Tp2ppQGRqGOhuVQgbqs6yJ33GKXo7RPsJVLw3FR7XAxENIVqNvzGotbDXr0NjBGdzyxIHzrOaUqM4w1iLzD1KF51dXFwkoigqDdD7Ze9eI_Uo3tSn8FwGLTSoO-ONQYpnceCiGut2Gc6VIL8HOLdh8dzlRENGQtgYd-3Y5zqpoLrsR2Bd-0sv15sF-5aI0CqcC8gE70JPImKf2u_IYI-TYMDNk86YSCtaYO5-alOrHXXWwgzSoH-r2s5qoOhPbei9myV_P4fdcKXxMqfap9hImXPUooVhpdUr1AabZw3MtW7rION8tJAiauhMQA"
const serviceAccountTokenRepeatedlyImpersonated = "Bearer eyJraWQiOiJaVFJqWlRNek5tSmlNRGt3TldJMU5USTRZVGxpT1RjMllUWXlZVE16WldNIiwiYWxnIjoiUlM1MTIifQ.eyJzdWIiOiIxNzM0YjRiNi0xZDVlLTQ4MTktOWI1MC0yOTkxN2ExYjlhZDUiLCJpc3MiOiJzdGFja2l0L3NlcnZpY2VhY2NvdW50IiwiYXVkIjpbInN0YWNraXQiLCJhcGkiXSwic3RhY2tpdC9zZXJ2aWNlYWNjb3VudC90b2tlbi5zb3VyY2UiOiJvYXV0aDIiLCJhY3QiOnsic3ViIjoiZjQ1MDA5YjItNjQzMy00M2MxLWI2YzctNjE4YzQ0MzU5ZTcxIiwiYWN0Ijp7InN1YiI6IjAyYWVmNTE2LTMxN2YtNGVjMS1hMWRmLTFhY2JkNGQ0OWZlMyJ9fSwic3RhY2tpdC9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJhcGkiLCJzdGFja2l0L3Byb2plY3QvcHJvamVjdC5pZCI6ImRhY2M3ODMwLTg0M2UtNGM1ZS04NmZmLWFhMGZiNTFkNjM2ZiIsImF6cCI6ImY0NTAwOWIyLTY0MzMtNDNjMS1iNmM3LTYxOGM0NDM1OWU3MSIsInN0YWNraXQvc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjE3MzRiNGI2LTFkNWUtNDgxOS05YjUwLTI5OTE3YTFiOWFkNSIsImV4cCI6MTcyNDA2Mjk2MywiaWF0IjoxNzI0MDU5MzYzLCJlbWFpbCI6InNlcnZpY2UtYWNjb3VudC0zLWZnaHN4dzFAc2Euc3RhY2tpdC5jbG91ZCIsImp0aSI6IjFmN2YxZWZjLTMzNDktNDExYS1hNWQ3LTIyNTVlMGE1YThhZSJ9.c1ae17bAtyOdmwXQbK37W-NTyOxo7iER5aHS_C0fU1qKl2BjOz708GLjH-_vxx9eKPeYznfI21_xlTaAvuG4Aco9f5YDK7fooTVHnDaOSSggqcEaDzDPrNXhhKEDxotJeq9zRMVCEStcbirjTounnLbuULRbO5GSY5jo-8n2UKxSZ2j5G_SjFHajdJwmzwvOttp08tdL8ck1uDdgVNBfcm0VIdb6WmgrCIUq5rmoa-cRPkdEurNtIEgEB_9U0Xh-SpmmsvFsWWeNIKz0e_5RCIyJonm_wMkGmblGegemkYL76ypeMNXTQsly1RozDIePfzHuZOWbySHSCd-vKQa2kw"
const serviceAccountTokenImpersonated = "Bearer eyJraWQiOiJaVFJqWlRNek5tSmlNRGt3TldJMU5USTRZVGxpT1RjMllUWXlZVE16WldNIiwiYWxnIjoiUlM1MTIifQ.eyJzdWIiOiJmNDUwMDliMi02NDMzLTQzYzEtYjZjNy02MThjNDQzNTllNzEiLCJpc3MiOiJzdGFja2l0L3NlcnZpY2VhY2NvdW50IiwiYXVkIjpbInN0YWNraXQiLCJhcGkiXSwic3RhY2tpdC9zZXJ2aWNlYWNjb3VudC90b2tlbi5zb3VyY2UiOiJvYXV0aDIiLCJhY3QiOnsic3ViIjoiMDJhZWY1MTYtMzE3Zi00ZWMxLWExZGYtMWFjYmQ0ZDQ5ZmUzIn0sInN0YWNraXQvc2VydmljZWFjY291bnQvbmFtZXNwYWNlIjoiYXBpIiwic3RhY2tpdC9wcm9qZWN0L3Byb2plY3QuaWQiOiJkYWNjNzgzMC04NDNlLTRjNWUtODZmZi1hYTBmYjUxZDYzNmYiLCJhenAiOiIwMmFlZjUxNi0zMTdmLTRlYzEtYTFkZi0xYWNiZDRkNDlmZTMiLCJzdGFja2l0L3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJmNDUwMDliMi02NDMzLTQzYzEtYjZjNy02MThjNDQzNTllNzEiLCJleHAiOjE3MjQwNjI5MDcsImlhdCI6MTcyNDA1OTMwNywiZW1haWwiOiJzZXJ2aWNlLWFjY291bnQtMi10ajlzcnQxQHNhLnN0YWNraXQuY2xvdWQiLCJqdGkiOiIzNzU1NTE4My0wMWI5LTQyNzAtYmRjMS02OWI0ZmNmZDVlZTkifQ.auBvvsIesFMAlWOCPCPC77DrrHF7gSKZwKs_Zry5KFvu2bpZZC1BcSXOc8b9eh0SzANI9M9aGJBhOzOm39-ZZ5XOQ-6_y1aWuEenYQ6kT5D3GzCUTMDzSi1lcZ4IG5nFMa_AAlVEN_7AMv7LHGtz49bWLJnAgeTo1cvof-OgP4mCQ5O6E0iyAq-5u8V8NJL7HIZy7BDe4J1mjfYhwKagrN7QFWu4fhN4TNS7d922X_6V489BhjRFRYjLW_qDnv912JorbGRz_XwNy_dPA81EkdMyKE0BJUezguJUEKEG2_JEi9O64Flcoi6x8cFHYhaDuMMSLipzePaHdyk2lQtH7Q"
const serviceAccountToken = "Bearer eyJraWQiOiJaVFJqWlRNek5tSmlNRGt3TldJMU5USTRZVGxpT1RjMllUWXlZVE16WldNIiwiYWxnIjoiUlM1MTIifQ.eyJzdWIiOiIxMGYzOGIwMS01MzRiLTQ3YmItYTAzYS1lMjk0Y2EyYmU0ZGUiLCJhdWQiOlsic3RhY2tpdCIsImFwaSJdLCJzdGFja2l0L3NlcnZpY2VhY2NvdW50L3Rva2VuLnNvdXJjZSI6ImxlZ2FjeSIsInN0YWNraXQvc2VydmljZWFjY291bnQvbmFtZXNwYWNlIjoiYXBpIiwic3RhY2tpdC9wcm9qZWN0L3Byb2plY3QuaWQiOiJkYWNjNzgzMC04NDNlLTRjNWUtODZmZi1hYTBmYjUxZDYzNmYiLCJhenAiOiJjZDk0ZjAxYS1kZjJlLTQ0NTYtOTAyZS00OGY1ZTU3ZjBiNjMiLCJpc3MiOiJzdGFja2l0L3NlcnZpY2VhY2NvdW50Iiwic3RhY2tpdC9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiMTBmMzhiMDEtNTM0Yi00N2JiLWEwM2EtZTI5NGNhMmJlNGRlIiwiZXhwIjoxNzIyNjY5MzQzLCJpYXQiOjE3MjI1ODI5NDMsImVtYWlsIjoibXktc2VydmljZS15aWZjOWUxQHNhLnN0YWNraXQuY2xvdWQiLCJqdGkiOiI4NGMzMGE0Ni0xMDAxLTQzNmYtODU5Zi04OWMwYmExOWJlMWUifQ.hb8X9VKc9xViHgNMyFHT9ePj_lyEwTV1D2es8E278WtoCJ9-4GPPQGjhcLGGrigjnvpRYV2LKzNqpQslerT5lFT_pHACsryaAE0ImYjmoe-nutA7BBpYuM_JN6pk5VIjVFLTqRKeIvFexPacqS2Vo3YoK1GvxPB8WPWBbGIsBtMl-PTm8OTwwzooBOoCRhhMR-E1lFbAymLsc1JI4yDQKLLomvhEopgmocCnQ-P1QkiKMqdkNxiD_YYLLYTOApg6d62BhqpH66ziqx493AStdZ8d5Kjvf3e1knDhaxVwNCghQj7lSo2kNAqZe__g2tiXpiZNTXBFJ_5HgQMLh67wng"
const userToken = "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOGJlZjc1LWRmY2QtNGE3My1hMzkxLTU0YTdhZjU3YTdkNiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic3RhY2tpdC1wb3J0YWwtbG9naW4tZGV2LWNsaWVudC1pZCJdLCJjbGllbnRfaWQiOiJzdGFja2l0LXBvcnRhbC1sb2dpbi1kZXYtY2xpZW50LWlkIiwiZW1haWwiOiJDaHJpc3RpYW4uU2NoYWlibGVAbm92YXRlYy1nbWJoLmRlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6MTcyMjU5MDM2NywiaWF0IjoxNzIyNTg2NzY3LCJpc3MiOiJodHRwczovL2FjY291bnRzLmRldi5zdGFja2l0LmNsb3VkIiwianRpIjoiZDczYTY3YWMtZDFlYy00YjU1LTk5ZDQtZTk1MzI3NWYwMjJhIiwibmJmIjoxNzIyNTg2NzY3LCJzY29wZSI6Im9wZW5pZCBlbWFpbCIsInN1YiI6ImNkOTRmMDFhLWRmMmUtNDQ1Ni05MDJlLTQ4ZjVlNTdmMGI2MyJ9.ajhjYbC5l5g7un9NSheoAwBT83YcZM91rH4DJxPTDsB78HzIVrmaKTPrK3AI_E1THlD2Z3_ot9nFr_eX7XcwWp_ZBlataKmakdXlAmeb4xSMGNYefIfzV_3w9ZZAZ66yoeTrtn8dUx5ezquenCYpctB1NcccmK4U09V0kNcq9dFcfF3Sg9YilF3orUCR0ql1d9RnOs3EiFZuUpdBEkyoVsAdSh2P-PRbNViR_FgCcAJem97TsN5CQc9RlvKYe4sYKgqQoqa2GDVi9Niiw3fe1V8SCnROYcpkOzBBWdvuzFMBUjln3uOogYVOz93xkmImV6jidgyQ70fLt-eDUmZZfg"
var TestHeaders = map[string][]string{"user-agent": {"custom"}, "authorization": {userToken}}
func newOrganizationAuditEvent(
customization *func(
*auditV1.AuditLogEntry,
*auditV1.ObjectIdentifier,
)) (
*auditV1.AuditLogEntry,
*auditV1.ObjectIdentifier,
) {
identifier := uuid.New()
permission := "resourcemanager.organization.edit"
permissionGranted := true
requestId := fmt.Sprintf("%s/1", identifier)
claims, _ := structpb.NewStruct(map[string]interface{}{})
correlationId := "cad100e2-e139-43b9-8c3b-335731e032bc"
headers := make(map[string]string)
headers["Content-Type"] = "application/json"
labels := make(map[string]string)
labels["label1"] = "value1"
auditEvent := &auditV1.AuditLogEntry{
LogName: fmt.Sprintf("%s/%s/logs/%s", ObjectTypeOrganization.Plural(), identifier, EventTypeAdminActivity),
ProtoPayload: &auditV1.AuditLog{
ServiceName: "resource-manager",
OperationName: "stackit.resourcemanager.v2.organization.created",
ResourceName: fmt.Sprintf("%s/%s", ObjectTypeOrganization.Plural(), identifier),
AuthenticationInfo: &auditV1.AuthenticationInfo{
PrincipalId: uuid.NewString(),
PrincipalEmail: "user@example.com",
ServiceAccountName: nil,
ServiceAccountDelegationInfo: nil,
},
AuthorizationInfo: []*auditV1.AuthorizationInfo{{
Resource: fmt.Sprintf("%s/%s", ObjectTypeOrganization.Plural(), identifier),
Permission: &permission,
Granted: &permissionGranted,
}},
RequestMetadata: &auditV1.RequestMetadata{
CallerIp: "127.0.0.1",
CallerSuppliedUserAgent: "OpenAPI-Generator/ 1.0.0/ go",
RequestAttributes: &auditV1.AttributeContext_Request{
Id: &requestId,
Method: auditV1.AttributeContext_HTTP_METHOD_POST,
Headers: headers,
Path: "/v2/organizations",
Host: "stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud",
Scheme: "https",
Query: nil,
Time: timestamppb.New(time.Now().UTC()),
Protocol: "http/1.1",
Auth: &auditV1.AttributeContext_Auth{
Principal: "https%3A%2F%2Faccounts.dev.stackit.cloud/stackit-resource-manager-dev",
Audiences: []string{"https:// stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud", "stackit", "api"},
Claims: claims,
},
},
},
Request: nil,
ResponseMetadata: &auditV1.ResponseMetadata{
StatusCode: wrapperspb.Int32(200),
ErrorMessage: nil,
ErrorDetails: nil,
ResponseAttributes: &auditV1.AttributeContext_Response{
NumResponseItems: nil,
Size: nil,
Headers: nil,
Time: timestamppb.New(time.Now().UTC()),
},
},
Response: nil,
Metadata: nil,
},
InsertId: fmt.Sprintf("%d/eu01/e72182e8-0bb9-4be2-a19f-87fc0dd6e738/00000000001", time.Now().UnixNano()),
Labels: labels,
CorrelationId: &correlationId,
Timestamp: timestamppb.New(time.Now()),
Severity: auditV1.LogSeverity_LOG_SEVERITY_DEFAULT,
TraceParent: nil,
TraceState: nil,
}
objectIdentifier := &auditV1.ObjectIdentifier{
Identifier: identifier.String(),
Type: string(ObjectTypeOrganization),
}
if customization != nil {
(*customization)(auditEvent, objectIdentifier)
}
return auditEvent, objectIdentifier
}
func newFolderAuditEvent(
customization *func(
*auditV1.AuditLogEntry,
*auditV1.ObjectIdentifier,
)) (
*auditV1.AuditLogEntry,
*auditV1.ObjectIdentifier,
) {
identifier := uuid.New()
permission := "resourcemanager.folder.edit"
permissionGranted := true
requestId := fmt.Sprintf("%s/1", identifier)
claims, _ := structpb.NewStruct(map[string]interface{}{})
correlationId := "9c71cedf-ca52-4f9c-a519-ed006e810cdd"
headers := make(map[string]string)
headers["Content-Type"] = "application/json"
labels := make(map[string]string)
labels["label1"] = "value1"
auditEvent := &auditV1.AuditLogEntry{
LogName: fmt.Sprintf("%s/%s/logs/%s", ObjectTypeFolder.Plural(), identifier, EventTypeAdminActivity),
ProtoPayload: &auditV1.AuditLog{
ServiceName: "resource-manager",
OperationName: "stackit.resourcemanager.v2.folder.created",
ResourceName: fmt.Sprintf("%s/%s", ObjectTypeFolder.Plural(), identifier),
AuthenticationInfo: &auditV1.AuthenticationInfo{
PrincipalId: uuid.NewString(),
PrincipalEmail: "user@example.com",
ServiceAccountName: nil,
ServiceAccountDelegationInfo: nil,
},
AuthorizationInfo: []*auditV1.AuthorizationInfo{{
Resource: fmt.Sprintf("%s/%s", ObjectTypeFolder.Plural(), identifier),
Permission: &permission,
Granted: &permissionGranted,
}},
RequestMetadata: &auditV1.RequestMetadata{
CallerIp: "127.0.0.1",
CallerSuppliedUserAgent: "OpenAPI-Generator/ 1.0.0/ go",
RequestAttributes: &auditV1.AttributeContext_Request{
Id: &requestId,
Method: auditV1.AttributeContext_HTTP_METHOD_POST,
Headers: headers,
Path: "/v2/folders",
Host: "stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud",
Scheme: "https",
Query: nil,
Time: timestamppb.New(time.Now().UTC()),
Protocol: "http/1.1",
Auth: &auditV1.AttributeContext_Auth{
Principal: "https%3A%2F%2Faccounts.dev.stackit.cloud/stackit-resource-manager-dev",
Audiences: []string{"https:// stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud", "stackit", "api"},
Claims: claims,
},
},
},
Request: nil,
ResponseMetadata: &auditV1.ResponseMetadata{
StatusCode: wrapperspb.Int32(200),
ErrorMessage: nil,
ErrorDetails: nil,
ResponseAttributes: &auditV1.AttributeContext_Response{
NumResponseItems: nil,
Size: nil,
Headers: nil,
Time: timestamppb.New(time.Now().UTC()),
},
},
Response: nil,
Metadata: nil,
},
InsertId: fmt.Sprintf("%d/eu01/e72182e8-0bb9-4be2-a19f-87fc0dd6e738/00000000001", time.Now().UnixNano()),
Labels: labels,
CorrelationId: &correlationId,
Timestamp: timestamppb.New(time.Now()),
Severity: auditV1.LogSeverity_LOG_SEVERITY_DEFAULT,
TraceParent: nil,
TraceState: nil,
}
objectIdentifier := &auditV1.ObjectIdentifier{
Identifier: identifier.String(),
Type: string(ObjectTypeFolder),
}
if customization != nil {
(*customization)(auditEvent, objectIdentifier)
}
return auditEvent, objectIdentifier
}
func newProjectAuditEvent(
customization *func(
*auditV1.AuditLogEntry,
*auditV1.ObjectIdentifier,
)) (
*auditV1.AuditLogEntry,
*auditV1.ObjectIdentifier,
) {
identifier := uuid.New()
permission := "resourcemanager.project.edit"
permissionGranted := true
requestId := fmt.Sprintf("%s/1", identifier)
claims, _ := structpb.NewStruct(map[string]interface{}{})
correlationId := "14d5b611-ccce-4cfa-9085-9ccbfccce3cb"
headers := make(map[string]string)
headers["Content-Type"] = "application/json"
labels := make(map[string]string)
labels["label1"] = "value1"
auditEvent := &auditV1.AuditLogEntry{
LogName: fmt.Sprintf("%s/%s/logs/%s", ObjectTypeProject.Plural(), identifier, EventTypeAdminActivity),
ProtoPayload: &auditV1.AuditLog{
ServiceName: "resource-manager",
OperationName: "stackit.resourcemanager.v2.project.created",
ResourceName: fmt.Sprintf("%s/%s", ObjectTypeProject.Plural(), identifier),
AuthenticationInfo: &auditV1.AuthenticationInfo{
PrincipalId: uuid.NewString(),
PrincipalEmail: "user@example.com",
ServiceAccountName: nil,
ServiceAccountDelegationInfo: nil,
},
AuthorizationInfo: []*auditV1.AuthorizationInfo{{
Resource: fmt.Sprintf("%s/%s", ObjectTypeProject.Plural(), identifier),
Permission: &permission,
Granted: &permissionGranted,
}},
RequestMetadata: &auditV1.RequestMetadata{
CallerIp: "127.0.0.1",
CallerSuppliedUserAgent: "OpenAPI-Generator/ 1.0.0/ go",
RequestAttributes: &auditV1.AttributeContext_Request{
Id: &requestId,
Method: auditV1.AttributeContext_HTTP_METHOD_POST,
Headers: headers,
Path: "/v2/projects",
Host: "stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud",
Scheme: "https",
Query: nil,
Time: timestamppb.New(time.Now().UTC()),
Protocol: "http/1.1",
Auth: &auditV1.AttributeContext_Auth{
Principal: "https%3A%2F%2Faccounts.dev.stackit.cloud/stackit-resource-manager-dev",
Audiences: []string{"https:// stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud", "stackit", "api"},
Claims: claims,
},
},
},
Request: nil,
ResponseMetadata: &auditV1.ResponseMetadata{
StatusCode: wrapperspb.Int32(200),
ErrorMessage: nil,
ErrorDetails: nil,
ResponseAttributes: &auditV1.AttributeContext_Response{
NumResponseItems: nil,
Size: nil,
Headers: nil,
Time: timestamppb.New(time.Now().UTC()),
},
},
Response: nil,
Metadata: nil,
},
InsertId: fmt.Sprintf("%d/eu01/e72182e8-0bb9-4be2-a19f-87fc0dd6e738/00000000001", time.Now().UnixNano()),
Labels: labels,
CorrelationId: &correlationId,
Timestamp: timestamppb.New(time.Now()),
Severity: auditV1.LogSeverity_LOG_SEVERITY_DEFAULT,
TraceParent: nil,
TraceState: nil,
}
objectIdentifier := &auditV1.ObjectIdentifier{
Identifier: identifier.String(),
Type: string(ObjectTypeProject),
}
if customization != nil {
(*customization)(auditEvent, objectIdentifier)
}
return auditEvent, objectIdentifier
}
func newProjectSystemAuditEvent(
customization *func(*auditV1.AuditLogEntry)) *auditV1.AuditLogEntry {
identifier := uuid.New()
requestId := fmt.Sprintf("%s/1", identifier)
claims, _ := structpb.NewStruct(map[string]interface{}{})
correlationId := "9b5a8e9b-32a0-435f-b97b-a9a42b9e016b"
headers := make(map[string]string)
headers["Content-Type"] = "application/json"
labels := make(map[string]string)
labels["label1"] = "value1"
serviceAccountId := uuid.NewString()
serviceAccountName := fmt.Sprintf("projects/%s/service-accounts/%s", identifier, serviceAccountId)
delegationPrincipal := auditV1.ServiceAccountDelegationInfo{Authority: &auditV1.ServiceAccountDelegationInfo_SystemPrincipal_{}}
auditEvent := &auditV1.AuditLogEntry{
LogName: fmt.Sprintf("%s/%s/logs/%s", SystemIdentifier.Type, SystemIdentifier.Identifier, EventTypeSystemEvent),
ProtoPayload: &auditV1.AuditLog{
ServiceName: "resource-manager",
OperationName: "stackit.resourcemanager.v2.system.changed",
ResourceName: fmt.Sprintf("%s/%s", ObjectTypeProject.Plural(), identifier),
AuthenticationInfo: &auditV1.AuthenticationInfo{
PrincipalId: serviceAccountId,
PrincipalEmail: "service-account@sa.stackit.cloud",
ServiceAccountName: &serviceAccountName,
ServiceAccountDelegationInfo: []*auditV1.ServiceAccountDelegationInfo{&delegationPrincipal},
},
AuthorizationInfo: []*auditV1.AuthorizationInfo{{
Resource: fmt.Sprintf("%s/%s", ObjectTypeProject.Plural(), identifier),
Permission: nil,
Granted: nil,
}},
RequestMetadata: &auditV1.RequestMetadata{
CallerIp: "127.0.0.1",
CallerSuppliedUserAgent: "OpenAPI-Generator/ 1.0.0/ go",
RequestAttributes: &auditV1.AttributeContext_Request{
Id: &requestId,
Method: auditV1.AttributeContext_HTTP_METHOD_POST,
Headers: headers,
Path: "/v2/projects",
Host: "stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud",
Scheme: "https",
Query: nil,
Time: timestamppb.New(time.Now().UTC()),
Protocol: "http/1.1",
Auth: &auditV1.AttributeContext_Auth{
Principal: "https%3A%2F%2Faccounts.dev.stackit.cloud/stackit-resource-manager-dev",
Audiences: []string{"https:// stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud", "stackit", "api"},
Claims: claims,
},
},
},
Request: nil,
ResponseMetadata: &auditV1.ResponseMetadata{
StatusCode: wrapperspb.Int32(200),
ErrorMessage: nil,
ErrorDetails: nil,
ResponseAttributes: &auditV1.AttributeContext_Response{
NumResponseItems: nil,
Size: nil,
Headers: nil,
Time: timestamppb.New(time.Now().UTC()),
},
},
Response: nil,
Metadata: nil,
},
InsertId: fmt.Sprintf("%d/eu01/e72182e8-0bb9-4be2-a19f-87fc0dd6e738/00000000001", time.Now().UnixNano()),
Labels: labels,
CorrelationId: &correlationId,
Timestamp: timestamppb.New(time.Now()),
Severity: auditV1.LogSeverity_LOG_SEVERITY_DEFAULT,
TraceParent: nil,
TraceState: nil,
}
if customization != nil {
(*customization)(auditEvent)
}
return auditEvent
}
func newSystemAuditEvent(
customization *func(*auditV1.AuditLogEntry)) *auditV1.AuditLogEntry {
identifier := uuid.Nil
requestId := fmt.Sprintf("%s/1", identifier)
claims, _ := structpb.NewStruct(map[string]interface{}{})
correlationId := "14d5b611-ccce-4cfa-9085-9ccbfccce3cb"
headers := make(map[string]string)
headers["Content-Type"] = "application/json"
labels := make(map[string]string)
labels["label1"] = "value1"
serviceAccountId := uuid.NewString()
serviceAccountName := fmt.Sprintf("projects/%s/service-accounts/%s", identifier, serviceAccountId)
delegationPrincipal := auditV1.ServiceAccountDelegationInfo{Authority: &auditV1.ServiceAccountDelegationInfo_SystemPrincipal_{}}
auditEvent := &auditV1.AuditLogEntry{
LogName: fmt.Sprintf("%s/%s/logs/%s", ObjectTypeSystem.Plural(), identifier, EventTypeSystemEvent),
ProtoPayload: &auditV1.AuditLog{
ServiceName: "resource-manager",
OperationName: "stackit.resourcemanager.v2.system.changed",
ResourceName: fmt.Sprintf("%s/%s", ObjectTypeSystem.Plural(), identifier),
AuthenticationInfo: &auditV1.AuthenticationInfo{
PrincipalId: serviceAccountId,
PrincipalEmail: "service-account@sa.stackit.cloud",
ServiceAccountName: &serviceAccountName,
ServiceAccountDelegationInfo: []*auditV1.ServiceAccountDelegationInfo{&delegationPrincipal},
},
AuthorizationInfo: []*auditV1.AuthorizationInfo{{
Resource: fmt.Sprintf("%s/%s", ObjectTypeSystem.Plural(), identifier),
Permission: nil,
Granted: nil,
}},
RequestMetadata: &auditV1.RequestMetadata{
CallerIp: "127.0.0.1",
CallerSuppliedUserAgent: "OpenAPI-Generator/ 1.0.0/ go",
RequestAttributes: &auditV1.AttributeContext_Request{
Id: &requestId,
Method: auditV1.AttributeContext_HTTP_METHOD_POST,
Headers: headers,
Path: "/v2/projects",
Host: "stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud",
Scheme: "https",
Query: nil,
Time: timestamppb.New(time.Now().UTC()),
Protocol: "http/1.1",
Auth: &auditV1.AttributeContext_Auth{
Principal: "https%3A%2F%2Faccounts.dev.stackit.cloud/stackit-resource-manager-dev",
Audiences: []string{"https:// stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud", "stackit", "api"},
Claims: claims,
},
},
},
Request: nil,
ResponseMetadata: &auditV1.ResponseMetadata{
StatusCode: wrapperspb.Int32(200),
ErrorMessage: nil,
ErrorDetails: nil,
ResponseAttributes: &auditV1.AttributeContext_Response{
NumResponseItems: nil,
Size: nil,
Headers: nil,
Time: timestamppb.New(time.Now().UTC()),
},
},
Response: nil,
Metadata: nil,
},
InsertId: fmt.Sprintf("%d/eu01/e72182e8-0bb9-4be2-a19f-87fc0dd6e738/00000000001", time.Now().UnixNano()),
Labels: labels,
CorrelationId: &correlationId,
Timestamp: timestamppb.New(time.Now()),
Severity: auditV1.LogSeverity_LOG_SEVERITY_DEFAULT,
TraceParent: nil,
TraceState: nil,
}
if customization != nil {
(*customization)(auditEvent)
}
return auditEvent
}

View file

@ -0,0 +1,218 @@
package messaging
import (
"context"
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/log"
"errors"
"fmt"
"github.com/Azure/go-amqp"
"strings"
"sync"
"time"
)
// Default connection timeout for the AMQP connection
const connectionTimeoutSeconds = 10
// Api is an abstraction for a messaging system that can be used to send
// audit logs to the audit log system.
type Api interface {
// Send method will send the given data to the specified topic synchronously.
// Parameters:
// * ctx - the context object
// * topic - the messaging topic where to send the data to
// * data - the serialized data as byte array
// * contentType - the contentType of the serialized data
// * applicationProperties - properties to send with the message (i.e. cloud event headers)
//
// It returns technical errors for connection issues or sending problems.
Send(ctx context.Context, topic string, data []byte, contentType string, applicationProperties map[string]any) error
}
// MutexApi is wrapper around an API implementation that controls mutual exclusive access to the api.
type MutexApi struct {
mutex *sync.Mutex
api *Api
}
func NewMutexApi(api *Api) (*Api, error) {
if api == nil {
return nil, errors.New("api is nil")
}
mutexApi := MutexApi{
mutex: &sync.Mutex{},
api: api,
}
var genericApi Api = &mutexApi
return &genericApi, nil
}
// Send implements Api.Send
func (m *MutexApi) Send(ctx context.Context, topic string, data []byte, contentType string, applicationProperties map[string]any) error {
m.mutex.Lock()
defer m.mutex.Unlock()
return (*m.api).Send(ctx, topic, data, contentType, applicationProperties)
}
// AmqpConfig provides AMQP connection related parameters.
type AmqpConfig struct {
URL string
User string
Password string
}
// AmqpSession is an abstraction providing a subset of the methods of amqp.Session
type AmqpSession interface {
NewSender(ctx context.Context, target string, opts *amqp.SenderOptions) (*AmqpSender, error)
Close(ctx context.Context) error
}
type AmqpSessionWrapper struct {
session *amqp.Session
}
func (w AmqpSessionWrapper) NewSender(ctx context.Context, target string, opts *amqp.SenderOptions) (*AmqpSender, error) {
sender, err := w.session.NewSender(ctx, target, opts)
var amqpSender AmqpSender = sender
return &amqpSender, err
}
func (w AmqpSessionWrapper) Close(ctx context.Context) error {
return w.session.Close(ctx)
}
// AmqpSender is an abstraction providing a subset of the methods of amqp.Sender
type AmqpSender interface {
Send(ctx context.Context, msg *amqp.Message, opts *amqp.SendOptions) error
Close(ctx context.Context) error
}
// AmqpApi implements Api.
type AmqpApi struct {
config AmqpConfig
connection *amqp.Conn
session *AmqpSession
}
func NewAmqpApi(amqpConfig AmqpConfig) (*Api, error) {
amqpApi := &AmqpApi{config: amqpConfig}
err := amqpApi.connect()
if err != nil {
return nil, err
}
var messagingApi Api = amqpApi
return &messagingApi, nil
}
// connect opens a new connection and session to the AMQP messaging system.
// The connection attempt will be cancelled after connectionTimeoutSeconds.
func (a *AmqpApi) connect() error {
log.AuditLogger.Info("connecting to messaging system")
// Set credentials if specified
auth := amqp.SASLTypeAnonymous()
if a.config.User != "" && a.config.Password != "" {
auth = amqp.SASLTypePlain(a.config.User, a.config.Password)
log.AuditLogger.Info("using username and password for messaging")
} else {
log.AuditLogger.Warn("using anonymous messaging!")
}
options := &amqp.ConnOptions{
SASLType: auth,
}
// Create new context with timeout for the connection initialization
subCtx, cancel := context.WithTimeout(context.Background(), connectionTimeoutSeconds*time.Second)
defer cancel()
// Initialize connection
conn, err := amqp.Dial(subCtx, a.config.URL, options)
if err != nil {
return err
}
a.connection = conn
// Initialize session
session, err := conn.NewSession(context.Background(), nil)
if err != nil {
return err
}
var amqpSession AmqpSession = AmqpSessionWrapper{session: session}
a.session = &amqpSession
return nil
}
// Send implements Api.Send.
// If errors occur the connection to the messaging system will be closed and re-established.
func (a *AmqpApi) Send(ctx context.Context, topic string, data []byte, contentType string, applicationProperties map[string]any) error {
err := a.trySend(ctx, topic, data, contentType, applicationProperties)
if err == nil {
return nil
}
// Drop the current sender, as it cannot connect to the broker anymore
log.AuditLogger.Error("message sender error, recreating", err)
err = a.resetConnection(ctx)
if err != nil {
return err
}
return a.trySend(ctx, topic, data, contentType, applicationProperties)
}
// trySend actually sends the given data as amqp.Message to the messaging system.
func (a *AmqpApi) trySend(ctx context.Context, topic string, data []byte, contentType string, applicationProperties map[string]any) error {
if !strings.HasPrefix(topic, AmqpTopicPrefix) {
return fmt.Errorf(
"topic %q name lacks mandatory prefix %q",
topic,
AmqpTopicPrefix,
)
}
sender, err := (*a.session).NewSender(ctx, topic, nil)
if err != nil {
return err
}
bytes := [][]byte{data}
message := amqp.Message{
Header: &amqp.MessageHeader{
Durable: true,
},
Properties: &amqp.MessageProperties{
To: &topic,
ContentType: &contentType,
},
ApplicationProperties: applicationProperties,
Data: bytes,
}
err = (*sender).Send(ctx, &message, nil)
if err != nil {
_ = (*sender).Close(ctx)
return err
}
return nil
}
// resetConnection closes the current session and connection and reconnects to the messaging system.
func (a *AmqpApi) resetConnection(ctx context.Context) error {
_ = (*a.session).Close(ctx)
err := a.connection.Close()
if err != nil {
log.AuditLogger.Error("failed to close message connection", err)
}
return a.connect()
}

View file

@ -0,0 +1,163 @@
package messaging
import (
"context"
"errors"
"fmt"
"github.com/Azure/go-amqp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"testing"
"time"
)
type AmqpSessionMock struct {
mock.Mock
}
func (m *AmqpSessionMock) NewSender(ctx context.Context, target string, opts *amqp.SenderOptions) (*AmqpSender, error) {
args := m.Called(ctx, target, opts)
var sender *AmqpSender = nil
if args.Get(0) != nil {
sender = args.Get(0).(*AmqpSender)
}
err := args.Error(1)
return sender, err
}
func (m *AmqpSessionMock) Close(ctx context.Context) error {
args := m.Called(ctx)
return args.Error(0)
}
type AmqpSenderMock struct {
mock.Mock
}
func (m *AmqpSenderMock) Send(ctx context.Context, msg *amqp.Message, opts *amqp.SendOptions) error {
args := m.Called(ctx, msg, opts)
return args.Error(0)
}
func (m *AmqpSenderMock) Close(ctx context.Context) error {
args := m.Called(ctx)
return args.Error(0)
}
func Test_NewAmqpMessagingApi(t *testing.T) {
_, err := NewAmqpApi(AmqpConfig{URL: "not-handled-protocol://localhost:5672"})
assert.EqualError(t, err, "unsupported scheme \"not-handled-protocol\"")
}
func Test_AmqpMessagingApi_Send(t *testing.T) {
// Specify test timeout
ctx, cancelFn := context.WithTimeout(context.Background(), 120*time.Second)
defer cancelFn()
// Start solace docker container
solaceContainer, err := NewSolaceContainer(context.Background())
assert.NoError(t, err)
defer solaceContainer.Stop()
t.Run("Missing topic prefix", func(t *testing.T) {
defer solaceContainer.StopOnError()
api, err := NewAmqpApi(AmqpConfig{URL: solaceContainer.AmqpConnectionString})
assert.NoError(t, err)
err = (*api).Send(ctx, "topic-name", []byte{}, "application/json", make(map[string]any))
assert.EqualError(t, err, "topic \"topic-name\" name lacks mandatory prefix \"topic://\"")
})
t.Run("New sender call returns error", func(t *testing.T) {
defer solaceContainer.StopOnError()
// Initialize the solace queue
topicSubscriptionTopicPattern := "auditlog/>"
queueName := "messaging-new-sender"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
topicName := fmt.Sprintf("topic://auditlog/%s", "amqp-no-new-sender")
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
api := &AmqpApi{config: AmqpConfig{URL: solaceContainer.AmqpConnectionString}}
err := api.connect()
assert.NoError(t, err)
expectedError := errors.New("expected error")
// Set mock session
sessionMock := AmqpSessionMock{}
sessionMock.On("NewSender", mock.Anything, mock.Anything, mock.Anything).Return(nil, expectedError)
sessionMock.On("Close", mock.Anything).Return(nil)
var amqpSession AmqpSession = &sessionMock
api.session = &amqpSession
// It's expected that the test succeeds.
// First the session is closed as it returns the expected error
// Then the retry mechanism restarts the connection and successfully sends the data
value := "test"
err = (*api).Send(ctx, topicName, []byte(value), "application/json", make(map[string]any))
assert.NoError(t, err)
// Check that the mock was called
assert.True(t, sessionMock.AssertNumberOfCalls(t, "NewSender", 1))
assert.True(t, sessionMock.AssertNumberOfCalls(t, "Close", 1))
message, err := solaceContainer.NextMessage(ctx, fmt.Sprintf("queue://%s", queueName), true)
assert.NoError(t, err)
assert.Equal(t, value, string(message.Data[0]))
assert.Equal(t, topicName, *message.Properties.To)
})
t.Run("Send call on sender returns error", func(t *testing.T) {
defer solaceContainer.StopOnError()
// Initialize the solace queue
topicSubscriptionTopicPattern := "auditlog/>"
queueName := "messaging-sender-error"
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
topicName := fmt.Sprintf("topic://auditlog/%s", "amqp-sender-error")
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
api := &AmqpApi{config: AmqpConfig{URL: solaceContainer.AmqpConnectionString}}
err := api.connect()
assert.NoError(t, err)
expectedError := errors.New("expected error")
// Instantiate mock sender
senderMock := AmqpSenderMock{}
senderMock.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(expectedError)
senderMock.On("Close", mock.Anything).Return(nil)
var amqpSender AmqpSender = &senderMock
// Set mock session
sessionMock := AmqpSessionMock{}
sessionMock.On("NewSender", mock.Anything, mock.Anything, mock.Anything).Return(&amqpSender, nil)
sessionMock.On("Close", mock.Anything).Return(nil)
var amqpSession AmqpSession = &sessionMock
api.session = &amqpSession
// It's expected that the test succeeds.
// First the sender and session are closed as the sender returns the expected error
// Then the retry mechanism restarts the connection and successfully sends the data
value := "test"
err = (*api).Send(ctx, topicName, []byte(value), "application/json", make(map[string]any))
assert.NoError(t, err)
// Check that the mocks were called
assert.True(t, sessionMock.AssertNumberOfCalls(t, "NewSender", 1))
assert.True(t, sessionMock.AssertNumberOfCalls(t, "Close", 1))
assert.True(t, senderMock.AssertNumberOfCalls(t, "Send", 1))
assert.True(t, senderMock.AssertNumberOfCalls(t, "Close", 1))
message, err := solaceContainer.NextMessage(ctx, fmt.Sprintf("queue://%s", queueName), true)
assert.NoError(t, err)
assert.Equal(t, value, string(message.Data[0]))
assert.Equal(t, topicName, *message.Properties.To)
})
}

438
audit/messaging/solace.go Normal file
View file

@ -0,0 +1,438 @@
package messaging
import (
"bytes"
"context"
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/log"
"encoding/json"
"errors"
"fmt"
"github.com/Azure/go-amqp"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"io"
"net/http"
"regexp"
"strings"
"time"
)
const (
AmqpTopicPrefix = "topic://"
AmqpQueuePrefix = "queue://"
)
var ErrResourceNotFound = errors.New("resource not found")
type SempClient struct {
client http.Client
sempApiBaseUrl string
username string
password string
}
func (c SempClient) RequestWithoutBody(ctx context.Context, method string, url string) error {
request, err := http.NewRequestWithContext(ctx, method, fmt.Sprintf("%s%s", c.sempApiBaseUrl, url), nil)
if err != nil {
return err
}
response, err := c.doRequest(request)
if err != nil {
return err
}
_, err = c.parseResponseAsObject(response)
return err
}
func (c SempClient) RequestWithBody(ctx context.Context, method string, url string, body any) error {
data, err := json.Marshal(body)
if err != nil {
return err
}
request, err := http.NewRequestWithContext(ctx, method, fmt.Sprintf("%s%s", c.sempApiBaseUrl, url), bytes.NewBuffer(data))
if err != nil {
return err
}
response, err := c.doRequest(request)
if err != nil {
return err
}
_, err = c.parseResponseAsObject(response)
return err
}
func (c SempClient) doRequest(request *http.Request) ([]byte, error) {
request.SetBasicAuth(c.username, c.password)
if request.Method != http.MethodGet {
request.Header.Set("Content-Type", "application/json")
}
response, err := c.client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
rawBody, err := io.ReadAll(response.Body)
if err != nil || (response.StatusCode != http.StatusOK && response.StatusCode != http.StatusBadRequest) {
return nil, fmt.Errorf("request to %v failes with status %v (%v), response:\n%s", response.StatusCode, response.Status, request.URL, rawBody)
}
if _, err := io.Copy(io.Discard, response.Body); err != nil {
return nil, fmt.Errorf("response processing error for call to %v", request.URL)
}
return rawBody, nil
}
func (c SempClient) parseResponseAsObject(dataResponse []byte) (map[string]any, error) {
data := map[string]any{}
err := json.Unmarshal(dataResponse, &data)
if err != nil {
return nil, fmt.Errorf("could not parse response:\n%s", dataResponse)
}
rawData, ok := data["data"]
if ok {
data, _ = rawData.(map[string]any)
return data, nil
} else {
metadata, ok := data["meta"]
if ok {
data, _ = metadata.(map[string]any)
if data["responseCode"].(float64) == http.StatusOK {
// http-delete
return nil, nil
}
description := data["error"].(map[string]interface{})["description"].(string)
status := data["error"].(map[string]interface{})["status"].(string)
if status == "NOT_FOUND" {
// resource not found
return nil, fmt.Errorf("request failed - description: %v, status: %v, %w", description, status, ErrResourceNotFound)
}
return nil, fmt.Errorf("request failed - description: %v, status: %v", description, status)
}
}
return nil, fmt.Errorf("could not parse response:\n%s", dataResponse)
}
// SolaceContainer wraps a testcontainers docker container instance of solace.
//
// The container must be terminated by calling:
// solaceContainer.Terminate(ctx)
type SolaceContainer struct {
testcontainers.Container
AmqpConnectionString string
sempClient SempClient
}
// NewSolaceContainer starts a container and waits until it is ready to be used.
func NewSolaceContainer(ctx context.Context) (*SolaceContainer, error) {
env := make(map[string]string)
env["username_admin_globalaccesslevel"] = "admin"
env["username_admin_password"] = "admin"
// Start docker container
request := testcontainers.ContainerRequest{
Image: "solace/solace-pubsub-standard:10.8",
ExposedPorts: []string{"5672/tcp", "8080/tcp"},
SkipReaper: true,
AutoRemove: true,
ShmSize: 1024 * 1024 * 1024, // 1 GB,
Env: env,
WaitingFor: wait.ForLog("Running pre-startup checks:").
WithStartupTimeout(90 * time.Second),
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: request,
Started: true,
})
if err != nil {
return nil, err
}
// Extract host and port information
host, err := container.Host(ctx)
if err != nil {
_ = container.Terminate(ctx)
return nil, err
}
amqpPort, err := container.MappedPort(ctx, "5672")
if err != nil {
_ = container.Terminate(ctx)
return nil, err
}
sempPort, err := container.MappedPort(ctx, "8080")
if err != nil {
_ = container.Terminate(ctx)
return nil, err
}
log.AuditLogger.Info("UI Port: " + sempPort.Port())
// Construct connection strings
amqpConnectionString := fmt.Sprintf("amqp://%s:%s/", host, amqpPort.Port())
sempApiBaseUrl := fmt.Sprintf("http://%s:%s/SEMP/v2", host, sempPort.Port())
// Construct SEMP client
sempClient := SempClient{client: http.Client{}, sempApiBaseUrl: sempApiBaseUrl, username: "admin", password: "admin"}
// Poll queue endpoint until solace is ready to interact
solaceStarting := true
for solaceStarting {
err := sempClient.RequestWithoutBody(
ctx,
"GET",
"/config/msgVpns/default/queues/test",
)
if err != nil && strings.Contains(err.Error(), "NOT_FOUND") {
solaceStarting = false
}
time.Sleep(1000 * time.Millisecond)
}
// Return container object
return &SolaceContainer{
Container: container,
AmqpConnectionString: amqpConnectionString,
sempClient: sempClient,
}, nil
}
// QueueCreate creates a queue with the given name.
func (c SolaceContainer) QueueCreate(ctx context.Context, queueName string) error {
// Construct parameters
var queueConfig = make(map[string]any)
queueConfig["accessType"] = "non-exclusive"
queueConfig["egressEnabled"] = true
queueConfig["ingressEnabled"] = true
queueConfig["permission"] = "consume"
queueConfig["queueName"] = queueName
queueConfig["maxBindCount"] = 100
// Create the queue
err := c.sempClient.RequestWithBody(
ctx,
"POST",
"/config/msgVpns/default/queues",
queueConfig)
return err
}
// QueueExists checks if a queue with the given name exists.
func (c SolaceContainer) QueueExists(ctx context.Context, queueName string) (bool, error) {
// Check if exists
err := c.sempClient.RequestWithoutBody(
ctx,
"GET",
fmt.Sprintf("/config/msgVpns/default/queues/%s", queueName),
)
// Check if response contains "NOT_FOUND" string indicating that the queue doesn't exist
if err != nil {
if strings.Contains(err.Error(), "NOT_FOUND") {
return false, nil
}
// Return technical errors
return false, err
}
// Return queue exists
return true, nil
}
// QueueDeleteIfExists deletes the queue with the given name if it exists.
func (c SolaceContainer) QueueDeleteIfExists(ctx context.Context, queueName string) error {
// Check if queue exists
exists, err := c.QueueExists(ctx, queueName)
if err != nil {
return err
}
// Delete if exists
if exists {
err := c.sempClient.RequestWithoutBody(
ctx,
"DELETE",
fmt.Sprintf("/config/msgVpns/default/queues/%s", queueName),
)
return err
}
return nil
}
// TopicSubscriptionCreate creates a topic subscription for a (underlying) queue.
//
// Parameters:
// * ctx - the context object
// * queueName - the name of the queue where the topic(s) should be subscribed
// * topicName - the name of the topic with optional wildcards (e.g. "organizations/org-*")
func (c SolaceContainer) TopicSubscriptionCreate(ctx context.Context, queueName string, topicName string) error {
// Construct url and parameters
url := fmt.Sprintf("/config/msgVpns/default/queues/%s/subscriptions", queueName)
subscriptionConfig := make(map[string]any)
subscriptionConfig["subscriptionTopic"] = topicName
// Create the subscription
err := c.sempClient.RequestWithBody(ctx, "POST", url, subscriptionConfig)
return err
}
func (c SolaceContainer) NewAmqpConnection(ctx context.Context) (*amqp.Conn, error) {
return amqp.Dial(ctx, c.AmqpConnectionString, nil)
}
// ValidateTopicName checks that topicName and topicSubscriptionTopicPattern are valid and compatible
// Solace topic name constraints can be found here:
// https://docs.solace.com/Messaging/SMF-Topics.htm
func (c SolaceContainer) ValidateTopicName(topicSubscriptionTopicPattern string, topicName string) error {
// Cut off the topic:// prefix
var name string
if strings.HasPrefix(topicName, "topic://") {
name = topicName[len("topic://"):]
} else {
name = topicName
}
// Check input
if topicSubscriptionTopicPattern == "" {
return errors.New("topicSubscriptionTopicPattern is empty")
}
if name == "" {
return errors.New("topicName is empty")
}
// Check topic name
allowedTopicCharacters, err := regexp.Compile(`[0-9A-Za-z-.]+(?:/[0-9A-Za-z-.]+)+|[0-9A-Za-z-.]+`)
if err != nil {
return err
}
if !allowedTopicCharacters.MatchString(name) {
return errors.New("invalid topic name")
}
// 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-.]*\*)|/>)|>`)
if err != nil {
return err
}
if !allowedTopicSubscriptionCharacters.MatchString(topicSubscriptionTopicPattern) {
return errors.New("invalid topic subscription name")
}
// Check compatibility
subscriptionIndex := 0
var expectedNextCharacter uint8 = 0
var nextError error
for i := 0; i < len(name); i++ {
if expectedNextCharacter != 0 {
if expectedNextCharacter != name[i] {
return nextError
} else {
expectedNextCharacter = 0
nextError = nil
}
}
switch topicSubscriptionTopicPattern[subscriptionIndex] {
case '*':
if name[i] == '/' {
expectedNextCharacter = '/'
nextError = fmt.Errorf("invalid character '/' at index %d", i)
subscriptionIndex++
}
case '/':
if name[i] != '/' {
return fmt.Errorf("expected character '/', got %c at index %d", name[i], i)
}
subscriptionIndex++
case '>':
// everything is allowed
break
default:
if name[i] != topicSubscriptionTopicPattern[subscriptionIndex] {
return fmt.Errorf(
"expected character %c, got %c at index %d",
topicSubscriptionTopicPattern[subscriptionIndex],
name[i],
i,
)
} else {
subscriptionIndex++
}
}
}
return nil
}
// NextMessageFromQueue returns the next message from the queue.
// It is important that the topic subscription matches the topic name.
// Otherwise, no message is returned and the test will fail by exceeding the timeout.
func (c SolaceContainer) NextMessageFromQueue(
ctx context.Context,
queueName string,
accept bool,
) (*amqp.Message, error) {
return c.NextMessage(ctx, fmt.Sprintf("queue://%s", queueName), accept)
}
func (c SolaceContainer) NextMessage(ctx context.Context, target string, accept bool) (*amqp.Message, error) {
if !strings.HasPrefix(target, AmqpTopicPrefix) && !strings.HasPrefix(target, AmqpQueuePrefix) {
return nil, fmt.Errorf(
"solace receive: target %q name lacks mandatory prefix %q, %q",
target,
AmqpTopicPrefix,
AmqpQueuePrefix,
)
}
connection, err := c.NewAmqpConnection(ctx)
if err != nil {
return nil, err
}
session, err := connection.NewSession(ctx, nil)
if err != nil {
return nil, err
}
receiver, err := session.NewReceiver(ctx, target, nil)
if err != nil {
return nil, err
}
message, err := receiver.Receive(ctx, nil)
if err != nil {
return nil, err
}
if accept {
err := receiver.AcceptMessage(ctx, message)
if err != nil {
return nil, err
}
}
return message, nil
}
func (c SolaceContainer) Stop() {
_ = c.Terminate(context.Background())
}
func (c SolaceContainer) StopOnError() {
if r := recover(); r != nil {
c.Stop()
}
}

View file

@ -0,0 +1,45 @@
package utils
import "sync"
// SequenceNumberGenerator can be used to generate increasing numbers.
type SequenceNumberGenerator interface {
// Next returns the next number
Next() uint64
// Revert can be used to decrease the number (e.g. in case of an error)
Revert()
}
// DefaultSequenceNumberGenerator is a mutex protected implementation of SequenceNumberGenerator
type DefaultSequenceNumberGenerator struct {
sequenceNumber uint64
sequenceNumberLock sync.Mutex
}
// NewDefaultSequenceNumberGenerator returns an instance of DefaultSequenceNumberGenerator as pointer
// of SequenceNumberGenerator.
func NewDefaultSequenceNumberGenerator() *SequenceNumberGenerator {
var generator SequenceNumberGenerator = &DefaultSequenceNumberGenerator{
sequenceNumber: 0,
sequenceNumberLock: sync.Mutex{},
}
return &generator
}
// Next implements SequenceNumberGenerator.Next
func (g *DefaultSequenceNumberGenerator) Next() uint64 {
g.sequenceNumberLock.Lock()
defer g.sequenceNumberLock.Unlock()
next := g.sequenceNumber
g.sequenceNumber += 1
return next
}
// Revert implements SequenceNumberGenerator.Revert
func (g *DefaultSequenceNumberGenerator) Revert() {
g.sequenceNumberLock.Lock()
defer g.sequenceNumberLock.Unlock()
g.sequenceNumber -= 1
}

View file

@ -0,0 +1,22 @@
package utils
import (
"github.com/stretchr/testify/assert"
"testing"
)
func Test_DefaultSequenceNumberGenerator(t *testing.T) {
t.Run("next", func(t *testing.T) {
var sequenceGenerator = NewDefaultSequenceNumberGenerator()
assert.Equal(t, uint64(0), (*sequenceGenerator).Next())
})
t.Run("revert", func(t *testing.T) {
var sequenceGenerator = NewDefaultSequenceNumberGenerator()
assert.Equal(t, uint64(0), (*sequenceGenerator).Next())
assert.Equal(t, uint64(1), (*sequenceGenerator).Next())
(*sequenceGenerator).Revert()
assert.Equal(t, uint64(1), (*sequenceGenerator).Next())
})
}

2
buf.lock Normal file
View file

@ -0,0 +1,2 @@
# Generated by buf. DO NOT EDIT.
version: v1

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,543 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.35.1
// protoc (unknown)
// source: audit/v1/routable_event.proto
package auditV1
import (
_ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Visibility int32
const (
Visibility_VISIBILITY_UNSPECIFIED Visibility = 0
// Will be routed to customer data sinks
Visibility_VISIBILITY_PUBLIC Visibility = 1
// Will NOT be routed to customer data sinks
Visibility_VISIBILITY_PRIVATE Visibility = 2
)
// Enum value maps for Visibility.
var (
Visibility_name = map[int32]string{
0: "VISIBILITY_UNSPECIFIED",
1: "VISIBILITY_PUBLIC",
2: "VISIBILITY_PRIVATE",
}
Visibility_value = map[string]int32{
"VISIBILITY_UNSPECIFIED": 0,
"VISIBILITY_PUBLIC": 1,
"VISIBILITY_PRIVATE": 2,
}
)
func (x Visibility) Enum() *Visibility {
p := new(Visibility)
*p = x
return p
}
func (x Visibility) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Visibility) Descriptor() protoreflect.EnumDescriptor {
return file_audit_v1_routable_event_proto_enumTypes[0].Descriptor()
}
func (Visibility) Type() protoreflect.EnumType {
return &file_audit_v1_routable_event_proto_enumTypes[0]
}
func (x Visibility) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Visibility.Descriptor instead.
func (Visibility) EnumDescriptor() ([]byte, []int) {
return file_audit_v1_routable_event_proto_rawDescGZIP(), []int{0}
}
// Identifier of an object.
//
// For system events, the nil UUID must be used: 00000000-0000-0000-0000-000000000000.
type ObjectIdentifier struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Identifier of the respective entity (e.g. Identifier of an organization)
//
// Required: true
Identifier string `protobuf:"bytes,1,opt,name=identifier,proto3" json:"identifier,omitempty"`
// Entity data type relevant for routing - one of the list of supported object types.
//
// Required: true
Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"`
}
func (x *ObjectIdentifier) Reset() {
*x = ObjectIdentifier{}
mi := &file_audit_v1_routable_event_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ObjectIdentifier) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ObjectIdentifier) ProtoMessage() {}
func (x *ObjectIdentifier) ProtoReflect() protoreflect.Message {
mi := &file_audit_v1_routable_event_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ObjectIdentifier.ProtoReflect.Descriptor instead.
func (*ObjectIdentifier) Descriptor() ([]byte, []int) {
return file_audit_v1_routable_event_proto_rawDescGZIP(), []int{0}
}
func (x *ObjectIdentifier) GetIdentifier() string {
if x != nil {
return x.Identifier
}
return ""
}
func (x *ObjectIdentifier) GetType() string {
if x != nil {
return x.Type
}
return ""
}
type EncryptedData struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Encrypted serialized protobuf content (the actual audit event)
//
// Required: true
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
// Name of the protobuf type
//
// Required: true
ProtobufType string `protobuf:"bytes,2,opt,name=protobuf_type,json=protobufType,proto3" json:"protobuf_type,omitempty"`
// The password taken to derive the encryption key from
//
// Required: true
EncryptedPassword string `protobuf:"bytes,3,opt,name=encrypted_password,json=encryptedPassword,proto3" json:"encrypted_password,omitempty"`
// Version of the encrypted key
//
// Required: true
KeyVersion int32 `protobuf:"varint,4,opt,name=key_version,json=keyVersion,proto3" json:"key_version,omitempty"`
}
func (x *EncryptedData) Reset() {
*x = EncryptedData{}
mi := &file_audit_v1_routable_event_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EncryptedData) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EncryptedData) ProtoMessage() {}
func (x *EncryptedData) ProtoReflect() protoreflect.Message {
mi := &file_audit_v1_routable_event_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EncryptedData.ProtoReflect.Descriptor instead.
func (*EncryptedData) Descriptor() ([]byte, []int) {
return file_audit_v1_routable_event_proto_rawDescGZIP(), []int{1}
}
func (x *EncryptedData) GetData() []byte {
if x != nil {
return x.Data
}
return nil
}
func (x *EncryptedData) GetProtobufType() string {
if x != nil {
return x.ProtobufType
}
return ""
}
func (x *EncryptedData) GetEncryptedPassword() string {
if x != nil {
return x.EncryptedPassword
}
return ""
}
func (x *EncryptedData) GetKeyVersion() int32 {
if x != nil {
return x.KeyVersion
}
return 0
}
type UnencryptedData struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Unencrypted serialized protobuf content (the actual audit event)
//
// Required: true
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
// Name of the protobuf type
//
// Required: true
ProtobufType string `protobuf:"bytes,2,opt,name=protobuf_type,json=protobufType,proto3" json:"protobuf_type,omitempty"`
}
func (x *UnencryptedData) Reset() {
*x = UnencryptedData{}
mi := &file_audit_v1_routable_event_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UnencryptedData) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UnencryptedData) ProtoMessage() {}
func (x *UnencryptedData) ProtoReflect() protoreflect.Message {
mi := &file_audit_v1_routable_event_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UnencryptedData.ProtoReflect.Descriptor instead.
func (*UnencryptedData) Descriptor() ([]byte, []int) {
return file_audit_v1_routable_event_proto_rawDescGZIP(), []int{2}
}
func (x *UnencryptedData) GetData() []byte {
if x != nil {
return x.Data
}
return nil
}
func (x *UnencryptedData) GetProtobufType() string {
if x != nil {
return x.ProtobufType
}
return ""
}
type RoutableAuditEvent struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Functional event name with pattern
//
// Format: stackit.<product>.<version>.<type-chain>.<operation>
// Where:
//
// Product: The name of the service in lowercase
// Version: Optional API version
// Type-Chain: Chained path to object
// Operation: The name of the operation in lowercase
//
// Examples:
//
// "stackit.resource-manager.v1.organizations.create"
// "stackit.authorization.v1.projects.volumes.create"
// "stackit.authorization.v2alpha.projects.volumes.create"
// "stackit.authorization.v2.folders.move"
// "stackit.resource-manager.health"
//
// Required: true
OperationName string `protobuf:"bytes,1,opt,name=operation_name,json=operationName,proto3" json:"operation_name,omitempty"`
// Visibility relevant for differentiating between internal and public events
//
// Required: true
Visibility Visibility `protobuf:"varint,2,opt,name=visibility,proto3,enum=audit.v1.Visibility" json:"visibility,omitempty"`
// Identifier the audit log event refers to.
//
// System events, will not be routed to the end-user.
//
// Required: true
ObjectIdentifier *ObjectIdentifier `protobuf:"bytes,3,opt,name=object_identifier,json=objectIdentifier,proto3" json:"object_identifier,omitempty"`
// The actual audit event is transferred in one of the attributes below
//
// Required: true
//
// Types that are assignable to Data:
//
// *RoutableAuditEvent_UnencryptedData
// *RoutableAuditEvent_EncryptedData
Data isRoutableAuditEvent_Data `protobuf_oneof:"data"`
}
func (x *RoutableAuditEvent) Reset() {
*x = RoutableAuditEvent{}
mi := &file_audit_v1_routable_event_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RoutableAuditEvent) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RoutableAuditEvent) ProtoMessage() {}
func (x *RoutableAuditEvent) ProtoReflect() protoreflect.Message {
mi := &file_audit_v1_routable_event_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RoutableAuditEvent.ProtoReflect.Descriptor instead.
func (*RoutableAuditEvent) Descriptor() ([]byte, []int) {
return file_audit_v1_routable_event_proto_rawDescGZIP(), []int{3}
}
func (x *RoutableAuditEvent) GetOperationName() string {
if x != nil {
return x.OperationName
}
return ""
}
func (x *RoutableAuditEvent) GetVisibility() Visibility {
if x != nil {
return x.Visibility
}
return Visibility_VISIBILITY_UNSPECIFIED
}
func (x *RoutableAuditEvent) GetObjectIdentifier() *ObjectIdentifier {
if x != nil {
return x.ObjectIdentifier
}
return nil
}
func (m *RoutableAuditEvent) GetData() isRoutableAuditEvent_Data {
if m != nil {
return m.Data
}
return nil
}
func (x *RoutableAuditEvent) GetUnencryptedData() *UnencryptedData {
if x, ok := x.GetData().(*RoutableAuditEvent_UnencryptedData); ok {
return x.UnencryptedData
}
return nil
}
func (x *RoutableAuditEvent) GetEncryptedData() *EncryptedData {
if x, ok := x.GetData().(*RoutableAuditEvent_EncryptedData); ok {
return x.EncryptedData
}
return nil
}
type isRoutableAuditEvent_Data interface {
isRoutableAuditEvent_Data()
}
type RoutableAuditEvent_UnencryptedData struct {
UnencryptedData *UnencryptedData `protobuf:"bytes,4,opt,name=unencrypted_data,json=unencryptedData,proto3,oneof"`
}
type RoutableAuditEvent_EncryptedData struct {
EncryptedData *EncryptedData `protobuf:"bytes,5,opt,name=encrypted_data,json=encryptedData,proto3,oneof"`
}
func (*RoutableAuditEvent_UnencryptedData) isRoutableAuditEvent_Data() {}
func (*RoutableAuditEvent_EncryptedData) isRoutableAuditEvent_Data() {}
var File_audit_v1_routable_event_proto protoreflect.FileDescriptor
var file_audit_v1_routable_event_proto_rawDesc = []byte{
0x0a, 0x1d, 0x61, 0x75, 0x64, 0x69, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x61,
0x62, 0x6c, 0x65, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
0x08, 0x61, 0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x1a, 0x1b, 0x62, 0x75, 0x66, 0x2f, 0x76,
0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x5f, 0x0a, 0x10, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74,
0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x0a, 0x69, 0x64,
0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0b,
0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0a, 0x69, 0x64, 0x65,
0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x1e, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18,
0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0xc8, 0x01, 0x01, 0x72, 0x02, 0x10,
0x01, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xc5, 0x01, 0x0a, 0x0d, 0x45, 0x6e, 0x63, 0x72,
0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x12, 0x1e, 0x0a, 0x04, 0x64, 0x61, 0x74,
0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xba, 0x48, 0x07, 0xc8, 0x01, 0x01, 0x7a,
0x02, 0x10, 0x01, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2f, 0x0a, 0x0d, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x42, 0x0a, 0xba, 0x48, 0x07, 0xc8, 0x01, 0x01, 0x72, 0x02, 0x10, 0x01, 0x52, 0x0c, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x54, 0x79, 0x70, 0x65, 0x12, 0x39, 0x0a, 0x12, 0x65, 0x6e,
0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0xc8, 0x01, 0x01, 0x72, 0x02,
0x10, 0x01, 0x52, 0x11, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x50, 0x61, 0x73,
0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x28, 0x0a, 0x0b, 0x6b, 0x65, 0x79, 0x5f, 0x76, 0x65, 0x72,
0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x42, 0x07, 0xba, 0x48, 0x04, 0x1a,
0x02, 0x28, 0x01, 0x52, 0x0a, 0x6b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22,
0x62, 0x0a, 0x0f, 0x55, 0x6e, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61,
0x74, 0x61, 0x12, 0x1e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c,
0x42, 0x0a, 0xba, 0x48, 0x07, 0xc8, 0x01, 0x01, 0x7a, 0x02, 0x10, 0x01, 0x52, 0x04, 0x64, 0x61,
0x74, 0x61, 0x12, 0x2f, 0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x5f, 0x74,
0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0xc8, 0x01,
0x01, 0x72, 0x02, 0x10, 0x01, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x54,
0x79, 0x70, 0x65, 0x22, 0xb5, 0x03, 0x0a, 0x12, 0x52, 0x6f, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65,
0x41, 0x75, 0x64, 0x69, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x72, 0x0a, 0x0e, 0x6f, 0x70,
0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x42, 0x4b, 0xba, 0x48, 0x48, 0xc8, 0x01, 0x01, 0x72, 0x43, 0x32, 0x41, 0x5e, 0x73,
0x74, 0x61, 0x63, 0x6b, 0x69, 0x74, 0x5c, 0x2e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x2d,
0x5d, 0x2b, 0x5c, 0x2e, 0x28, 0x3f, 0x3a, 0x76, 0x5b, 0x30, 0x2d, 0x39, 0x5d, 0x2b, 0x5c, 0x2e,
0x29, 0x3f, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x2d, 0x2e, 0x5d, 0x2b,
0x5c, 0x2e, 0x29, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x2d, 0x5d, 0x2b, 0x24, 0x52,
0x0d, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x41,
0x0a, 0x0a, 0x76, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01,
0x28, 0x0e, 0x32, 0x14, 0x2e, 0x61, 0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x69,
0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01,
0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x0a, 0x76, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74,
0x79, 0x12, 0x4f, 0x0a, 0x11, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x65, 0x6e,
0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61,
0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64,
0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01,
0x52, 0x10, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69,
0x65, 0x72, 0x12, 0x46, 0x0a, 0x10, 0x75, 0x6e, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65,
0x64, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61,
0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70,
0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x48, 0x00, 0x52, 0x0f, 0x75, 0x6e, 0x65, 0x6e, 0x63,
0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x12, 0x40, 0x0a, 0x0e, 0x65, 0x6e,
0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x17, 0x2e, 0x61, 0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e,
0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x48, 0x00, 0x52, 0x0d, 0x65,
0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x42, 0x0d, 0x0a, 0x04,
0x64, 0x61, 0x74, 0x61, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x2a, 0x57, 0x0a, 0x0a, 0x56,
0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x12, 0x1a, 0x0a, 0x16, 0x56, 0x49, 0x53,
0x49, 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46,
0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, 0x56, 0x49, 0x53, 0x49, 0x42, 0x49, 0x4c,
0x49, 0x54, 0x59, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12,
0x56, 0x49, 0x53, 0x49, 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, 0x5f, 0x50, 0x52, 0x49, 0x56, 0x41,
0x54, 0x45, 0x10, 0x02, 0x42, 0x31, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x73, 0x63, 0x68, 0x77,
0x61, 0x72, 0x7a, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x69, 0x74, 0x2e, 0x61, 0x75, 0x64, 0x69,
0x74, 0x2e, 0x76, 0x31, 0x50, 0x01, 0x5a, 0x0f, 0x2e, 0x2f, 0x61, 0x75, 0x64, 0x69, 0x74, 0x3b,
0x61, 0x75, 0x64, 0x69, 0x74, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_audit_v1_routable_event_proto_rawDescOnce sync.Once
file_audit_v1_routable_event_proto_rawDescData = file_audit_v1_routable_event_proto_rawDesc
)
func file_audit_v1_routable_event_proto_rawDescGZIP() []byte {
file_audit_v1_routable_event_proto_rawDescOnce.Do(func() {
file_audit_v1_routable_event_proto_rawDescData = protoimpl.X.CompressGZIP(file_audit_v1_routable_event_proto_rawDescData)
})
return file_audit_v1_routable_event_proto_rawDescData
}
var file_audit_v1_routable_event_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_audit_v1_routable_event_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_audit_v1_routable_event_proto_goTypes = []any{
(Visibility)(0), // 0: audit.v1.Visibility
(*ObjectIdentifier)(nil), // 1: audit.v1.ObjectIdentifier
(*EncryptedData)(nil), // 2: audit.v1.EncryptedData
(*UnencryptedData)(nil), // 3: audit.v1.UnencryptedData
(*RoutableAuditEvent)(nil), // 4: audit.v1.RoutableAuditEvent
}
var file_audit_v1_routable_event_proto_depIdxs = []int32{
0, // 0: audit.v1.RoutableAuditEvent.visibility:type_name -> audit.v1.Visibility
1, // 1: audit.v1.RoutableAuditEvent.object_identifier:type_name -> audit.v1.ObjectIdentifier
3, // 2: audit.v1.RoutableAuditEvent.unencrypted_data:type_name -> audit.v1.UnencryptedData
2, // 3: audit.v1.RoutableAuditEvent.encrypted_data:type_name -> audit.v1.EncryptedData
4, // [4:4] is the sub-list for method output_type
4, // [4:4] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
}
func init() { file_audit_v1_routable_event_proto_init() }
func file_audit_v1_routable_event_proto_init() {
if File_audit_v1_routable_event_proto != nil {
return
}
file_audit_v1_routable_event_proto_msgTypes[3].OneofWrappers = []any{
(*RoutableAuditEvent_UnencryptedData)(nil),
(*RoutableAuditEvent_EncryptedData)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_audit_v1_routable_event_proto_rawDesc,
NumEnums: 1,
NumMessages: 4,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_audit_v1_routable_event_proto_goTypes,
DependencyIndexes: file_audit_v1_routable_event_proto_depIdxs,
EnumInfos: file_audit_v1_routable_event_proto_enumTypes,
MessageInfos: file_audit_v1_routable_event_proto_msgTypes,
}.Build()
File_audit_v1_routable_event_proto = out.File
file_audit_v1_routable_event_proto_rawDesc = nil
file_audit_v1_routable_event_proto_goTypes = nil
file_audit_v1_routable_event_proto_depIdxs = nil
}

View file

@ -0,0 +1,574 @@
// Code generated by protoc-gen-validate. DO NOT EDIT.
// source: audit/v1/routable_event.proto
package auditV1
import (
"bytes"
"errors"
"fmt"
"net"
"net/mail"
"net/url"
"regexp"
"sort"
"strings"
"time"
"unicode/utf8"
"google.golang.org/protobuf/types/known/anypb"
)
// ensure the imports are used
var (
_ = bytes.MinRead
_ = errors.New("")
_ = fmt.Print
_ = utf8.UTFMax
_ = (*regexp.Regexp)(nil)
_ = (*strings.Reader)(nil)
_ = net.IPv4len
_ = time.Duration(0)
_ = (*url.URL)(nil)
_ = (*mail.Address)(nil)
_ = anypb.Any{}
_ = sort.Sort
)
// Validate checks the field values on ObjectIdentifier with the rules defined
// in the proto definition for this message. If any rules are violated, the
// first error encountered is returned, or nil if there are no violations.
func (m *ObjectIdentifier) Validate() error {
return m.validate(false)
}
// ValidateAll checks the field values on ObjectIdentifier with the rules
// defined in the proto definition for this message. If any rules are
// violated, the result is a list of violation errors wrapped in
// ObjectIdentifierMultiError, or nil if none found.
func (m *ObjectIdentifier) ValidateAll() error {
return m.validate(true)
}
func (m *ObjectIdentifier) validate(all bool) error {
if m == nil {
return nil
}
var errors []error
// no validation rules for Identifier
// no validation rules for Type
if len(errors) > 0 {
return ObjectIdentifierMultiError(errors)
}
return nil
}
// ObjectIdentifierMultiError is an error wrapping multiple validation errors
// returned by ObjectIdentifier.ValidateAll() if the designated constraints
// aren't met.
type ObjectIdentifierMultiError []error
// Error returns a concatenation of all the error messages it wraps.
func (m ObjectIdentifierMultiError) Error() string {
var msgs []string
for _, err := range m {
msgs = append(msgs, err.Error())
}
return strings.Join(msgs, "; ")
}
// AllErrors returns a list of validation violation errors.
func (m ObjectIdentifierMultiError) AllErrors() []error { return m }
// ObjectIdentifierValidationError is the validation error returned by
// ObjectIdentifier.Validate if the designated constraints aren't met.
type ObjectIdentifierValidationError struct {
field string
reason string
cause error
key bool
}
// Field function returns field value.
func (e ObjectIdentifierValidationError) Field() string { return e.field }
// Reason function returns reason value.
func (e ObjectIdentifierValidationError) Reason() string { return e.reason }
// Cause function returns cause value.
func (e ObjectIdentifierValidationError) Cause() error { return e.cause }
// Key function returns key value.
func (e ObjectIdentifierValidationError) Key() bool { return e.key }
// ErrorName returns error name.
func (e ObjectIdentifierValidationError) ErrorName() string { return "ObjectIdentifierValidationError" }
// Error satisfies the builtin error interface
func (e ObjectIdentifierValidationError) Error() string {
cause := ""
if e.cause != nil {
cause = fmt.Sprintf(" | caused by: %v", e.cause)
}
key := ""
if e.key {
key = "key for "
}
return fmt.Sprintf(
"invalid %sObjectIdentifier.%s: %s%s",
key,
e.field,
e.reason,
cause)
}
var _ error = ObjectIdentifierValidationError{}
var _ interface {
Field() string
Reason() string
Key() bool
Cause() error
ErrorName() string
} = ObjectIdentifierValidationError{}
// Validate checks the field values on EncryptedData with the rules defined in
// the proto definition for this message. If any rules are violated, the first
// error encountered is returned, or nil if there are no violations.
func (m *EncryptedData) Validate() error {
return m.validate(false)
}
// ValidateAll checks the field values on EncryptedData with the rules defined
// in the proto definition for this message. If any rules are violated, the
// result is a list of violation errors wrapped in EncryptedDataMultiError, or
// nil if none found.
func (m *EncryptedData) ValidateAll() error {
return m.validate(true)
}
func (m *EncryptedData) validate(all bool) error {
if m == nil {
return nil
}
var errors []error
// no validation rules for Data
// no validation rules for ProtobufType
// no validation rules for EncryptedPassword
// no validation rules for KeyVersion
if len(errors) > 0 {
return EncryptedDataMultiError(errors)
}
return nil
}
// EncryptedDataMultiError is an error wrapping multiple validation errors
// returned by EncryptedData.ValidateAll() if the designated constraints
// aren't met.
type EncryptedDataMultiError []error
// Error returns a concatenation of all the error messages it wraps.
func (m EncryptedDataMultiError) Error() string {
var msgs []string
for _, err := range m {
msgs = append(msgs, err.Error())
}
return strings.Join(msgs, "; ")
}
// AllErrors returns a list of validation violation errors.
func (m EncryptedDataMultiError) AllErrors() []error { return m }
// EncryptedDataValidationError is the validation error returned by
// EncryptedData.Validate if the designated constraints aren't met.
type EncryptedDataValidationError struct {
field string
reason string
cause error
key bool
}
// Field function returns field value.
func (e EncryptedDataValidationError) Field() string { return e.field }
// Reason function returns reason value.
func (e EncryptedDataValidationError) Reason() string { return e.reason }
// Cause function returns cause value.
func (e EncryptedDataValidationError) Cause() error { return e.cause }
// Key function returns key value.
func (e EncryptedDataValidationError) Key() bool { return e.key }
// ErrorName returns error name.
func (e EncryptedDataValidationError) ErrorName() string { return "EncryptedDataValidationError" }
// Error satisfies the builtin error interface
func (e EncryptedDataValidationError) Error() string {
cause := ""
if e.cause != nil {
cause = fmt.Sprintf(" | caused by: %v", e.cause)
}
key := ""
if e.key {
key = "key for "
}
return fmt.Sprintf(
"invalid %sEncryptedData.%s: %s%s",
key,
e.field,
e.reason,
cause)
}
var _ error = EncryptedDataValidationError{}
var _ interface {
Field() string
Reason() string
Key() bool
Cause() error
ErrorName() string
} = EncryptedDataValidationError{}
// Validate checks the field values on UnencryptedData with the rules defined
// in the proto definition for this message. If any rules are violated, the
// first error encountered is returned, or nil if there are no violations.
func (m *UnencryptedData) Validate() error {
return m.validate(false)
}
// ValidateAll checks the field values on UnencryptedData with the rules
// defined in the proto definition for this message. If any rules are
// violated, the result is a list of violation errors wrapped in
// UnencryptedDataMultiError, or nil if none found.
func (m *UnencryptedData) ValidateAll() error {
return m.validate(true)
}
func (m *UnencryptedData) validate(all bool) error {
if m == nil {
return nil
}
var errors []error
// no validation rules for Data
// no validation rules for ProtobufType
if len(errors) > 0 {
return UnencryptedDataMultiError(errors)
}
return nil
}
// UnencryptedDataMultiError is an error wrapping multiple validation errors
// returned by UnencryptedData.ValidateAll() if the designated constraints
// aren't met.
type UnencryptedDataMultiError []error
// Error returns a concatenation of all the error messages it wraps.
func (m UnencryptedDataMultiError) Error() string {
var msgs []string
for _, err := range m {
msgs = append(msgs, err.Error())
}
return strings.Join(msgs, "; ")
}
// AllErrors returns a list of validation violation errors.
func (m UnencryptedDataMultiError) AllErrors() []error { return m }
// UnencryptedDataValidationError is the validation error returned by
// UnencryptedData.Validate if the designated constraints aren't met.
type UnencryptedDataValidationError struct {
field string
reason string
cause error
key bool
}
// Field function returns field value.
func (e UnencryptedDataValidationError) Field() string { return e.field }
// Reason function returns reason value.
func (e UnencryptedDataValidationError) Reason() string { return e.reason }
// Cause function returns cause value.
func (e UnencryptedDataValidationError) Cause() error { return e.cause }
// Key function returns key value.
func (e UnencryptedDataValidationError) Key() bool { return e.key }
// ErrorName returns error name.
func (e UnencryptedDataValidationError) ErrorName() string { return "UnencryptedDataValidationError" }
// Error satisfies the builtin error interface
func (e UnencryptedDataValidationError) Error() string {
cause := ""
if e.cause != nil {
cause = fmt.Sprintf(" | caused by: %v", e.cause)
}
key := ""
if e.key {
key = "key for "
}
return fmt.Sprintf(
"invalid %sUnencryptedData.%s: %s%s",
key,
e.field,
e.reason,
cause)
}
var _ error = UnencryptedDataValidationError{}
var _ interface {
Field() string
Reason() string
Key() bool
Cause() error
ErrorName() string
} = UnencryptedDataValidationError{}
// Validate checks the field values on RoutableAuditEvent with the rules
// defined in the proto definition for this message. If any rules are
// violated, the first error encountered is returned, or nil if there are no violations.
func (m *RoutableAuditEvent) Validate() error {
return m.validate(false)
}
// ValidateAll checks the field values on RoutableAuditEvent with the rules
// defined in the proto definition for this message. If any rules are
// violated, the result is a list of violation errors wrapped in
// RoutableAuditEventMultiError, or nil if none found.
func (m *RoutableAuditEvent) ValidateAll() error {
return m.validate(true)
}
func (m *RoutableAuditEvent) validate(all bool) error {
if m == nil {
return nil
}
var errors []error
// no validation rules for OperationName
// no validation rules for Visibility
if all {
switch v := interface{}(m.GetObjectIdentifier()).(type) {
case interface{ ValidateAll() error }:
if err := v.ValidateAll(); err != nil {
errors = append(errors, RoutableAuditEventValidationError{
field: "ObjectIdentifier",
reason: "embedded message failed validation",
cause: err,
})
}
case interface{ Validate() error }:
if err := v.Validate(); err != nil {
errors = append(errors, RoutableAuditEventValidationError{
field: "ObjectIdentifier",
reason: "embedded message failed validation",
cause: err,
})
}
}
} else if v, ok := interface{}(m.GetObjectIdentifier()).(interface{ Validate() error }); ok {
if err := v.Validate(); err != nil {
return RoutableAuditEventValidationError{
field: "ObjectIdentifier",
reason: "embedded message failed validation",
cause: err,
}
}
}
switch v := m.Data.(type) {
case *RoutableAuditEvent_UnencryptedData:
if v == nil {
err := RoutableAuditEventValidationError{
field: "Data",
reason: "oneof value cannot be a typed-nil",
}
if !all {
return err
}
errors = append(errors, err)
}
if all {
switch v := interface{}(m.GetUnencryptedData()).(type) {
case interface{ ValidateAll() error }:
if err := v.ValidateAll(); err != nil {
errors = append(errors, RoutableAuditEventValidationError{
field: "UnencryptedData",
reason: "embedded message failed validation",
cause: err,
})
}
case interface{ Validate() error }:
if err := v.Validate(); err != nil {
errors = append(errors, RoutableAuditEventValidationError{
field: "UnencryptedData",
reason: "embedded message failed validation",
cause: err,
})
}
}
} else if v, ok := interface{}(m.GetUnencryptedData()).(interface{ Validate() error }); ok {
if err := v.Validate(); err != nil {
return RoutableAuditEventValidationError{
field: "UnencryptedData",
reason: "embedded message failed validation",
cause: err,
}
}
}
case *RoutableAuditEvent_EncryptedData:
if v == nil {
err := RoutableAuditEventValidationError{
field: "Data",
reason: "oneof value cannot be a typed-nil",
}
if !all {
return err
}
errors = append(errors, err)
}
if all {
switch v := interface{}(m.GetEncryptedData()).(type) {
case interface{ ValidateAll() error }:
if err := v.ValidateAll(); err != nil {
errors = append(errors, RoutableAuditEventValidationError{
field: "EncryptedData",
reason: "embedded message failed validation",
cause: err,
})
}
case interface{ Validate() error }:
if err := v.Validate(); err != nil {
errors = append(errors, RoutableAuditEventValidationError{
field: "EncryptedData",
reason: "embedded message failed validation",
cause: err,
})
}
}
} else if v, ok := interface{}(m.GetEncryptedData()).(interface{ Validate() error }); ok {
if err := v.Validate(); err != nil {
return RoutableAuditEventValidationError{
field: "EncryptedData",
reason: "embedded message failed validation",
cause: err,
}
}
}
default:
_ = v // ensures v is used
}
if len(errors) > 0 {
return RoutableAuditEventMultiError(errors)
}
return nil
}
// RoutableAuditEventMultiError is an error wrapping multiple validation errors
// returned by RoutableAuditEvent.ValidateAll() if the designated constraints
// aren't met.
type RoutableAuditEventMultiError []error
// Error returns a concatenation of all the error messages it wraps.
func (m RoutableAuditEventMultiError) Error() string {
var msgs []string
for _, err := range m {
msgs = append(msgs, err.Error())
}
return strings.Join(msgs, "; ")
}
// AllErrors returns a list of validation violation errors.
func (m RoutableAuditEventMultiError) AllErrors() []error { return m }
// RoutableAuditEventValidationError is the validation error returned by
// RoutableAuditEvent.Validate if the designated constraints aren't met.
type RoutableAuditEventValidationError struct {
field string
reason string
cause error
key bool
}
// Field function returns field value.
func (e RoutableAuditEventValidationError) Field() string { return e.field }
// Reason function returns reason value.
func (e RoutableAuditEventValidationError) Reason() string { return e.reason }
// Cause function returns cause value.
func (e RoutableAuditEventValidationError) Cause() error { return e.cause }
// Key function returns key value.
func (e RoutableAuditEventValidationError) Key() bool { return e.key }
// ErrorName returns error name.
func (e RoutableAuditEventValidationError) ErrorName() string {
return "RoutableAuditEventValidationError"
}
// Error satisfies the builtin error interface
func (e RoutableAuditEventValidationError) Error() string {
cause := ""
if e.cause != nil {
cause = fmt.Sprintf(" | caused by: %v", e.cause)
}
key := ""
if e.key {
key = "key for "
}
return fmt.Sprintf(
"invalid %sRoutableAuditEvent.%s: %s%s",
key,
e.field,
e.reason,
cause)
}
var _ error = RoutableAuditEventValidationError{}
var _ interface {
Field() string
Reason() string
Key() bool
Cause() error
ErrorName() string
} = RoutableAuditEventValidationError{}

73
go.mod Normal file
View file

@ -0,0 +1,73 @@
module dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git
go 1.23.2
require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.1-20240920164238-5a7b106cbb87.1
github.com/Azure/go-amqp v1.2.0
github.com/bufbuild/protovalidate-go v0.7.2
github.com/google/uuid v1.6.0
github.com/rs/zerolog v1.33.0
github.com/stretchr/testify v1.9.0
github.com/testcontainers/testcontainers-go v0.34.0
go.opentelemetry.io/otel v1.31.0
go.opentelemetry.io/otel/trace v1.31.0
google.golang.org/protobuf v1.35.1
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/containerd/containerd v1.7.18 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v27.1.1+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/cel-go v0.21.0 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel/metric v1.31.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

223
go.sum Normal file
View file

@ -0,0 +1,223 @@
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.1-20240920164238-5a7b106cbb87.1 h1:9wP6ZZYWnF2Z0TxmII7m3XNykxnP4/w8oXeth6ekcRI=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.1-20240920164238-5a7b106cbb87.1/go.mod h1:Duw/9JoXkXIydyASnLYIiufkzySThoqavOsF+IihqvM=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-amqp v1.2.0 h1:NNyfN3/cRszWzMvjmm64yaPZDHX/2DJkowv8Ub9y01I=
github.com/Azure/go-amqp v1.2.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/bufbuild/protovalidate-go v0.7.2 h1:UuvKyZHl5p7u3ztEjtRtqtDxOjRKX5VUOgKFq6p6ETk=
github.com/bufbuild/protovalidate-go v0.7.2/go.mod h1:PHV5pFuWlRzdDW02/cmVyNzdiQ+RNNwo7idGxdzS7o4=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao=
github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI=
github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/testcontainers/testcontainers-go v0.34.0 h1:5fbgF0vIN5u+nD3IWabQwRybuB4GY8G2HHgCkbMzMHo=
github.com/testcontainers/testcontainers-go v0.34.0/go.mod h1:6P/kMkQe8yqPHfPWNulFGdFHTD8HB2vLq/231xY2iPQ=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda h1:b6F6WIV4xHHD0FA4oIyzU6mHWg2WI2X1RBehwa5QN38=
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda/go.mod h1:AHcE/gZH76Bk/ROZhQphlRoWo5xKDEtz3eVEO1LfA8c=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=

27
log/log.go Normal file
View file

@ -0,0 +1,27 @@
package log
import (
"errors"
"log/slog"
)
var AuditLogger Logger = SlogLogger{logger: slog.Default()}
type Logger interface {
Debug(msg string, err ...error)
Info(msg string, err ...error)
Warn(msg string, err ...error)
Error(msg string, err ...error)
}
func wrapErr(err []error) error {
var e error
if len(err) == 0 {
e = nil
} else if len(err) == 1 {
e = err[0]
} else {
e = errors.Join(err...)
}
return e
}

40
log/log_test.go Normal file
View file

@ -0,0 +1,40 @@
package log
import (
"errors"
"testing"
)
func Test_DefaultLogger(t *testing.T) {
t.Run("debug", func(t *testing.T) {
AuditLogger.Debug("debug message")
})
t.Run("debug with error details", func(t *testing.T) {
AuditLogger.Debug("debug message", errors.New("custom error"))
})
t.Run("info", func(t *testing.T) {
AuditLogger.Info("info message")
})
t.Run("info with error details", func(t *testing.T) {
AuditLogger.Info("info message", errors.New("custom error"))
})
t.Run("warn", func(t *testing.T) {
AuditLogger.Warn("warn message")
})
t.Run("warn with error details", func(t *testing.T) {
AuditLogger.Warn("warn message", errors.New("custom error"))
})
t.Run("error", func(t *testing.T) {
AuditLogger.Error("error message")
})
t.Run("error with error details", func(t *testing.T) {
AuditLogger.Error("error message", errors.New("custom error"))
})
}

35
log/slog.go Normal file
View file

@ -0,0 +1,35 @@
package log
import "log/slog"
type SlogLogger struct {
logger *slog.Logger
}
func UseSlogAuditLogger(logger *slog.Logger) {
AuditLogger = SlogLogger{logger: logger}
}
func (s SlogLogger) Debug(msg string, err ...error) {
s.logger.Debug(msg, s.getWrappedError(err))
}
func (s SlogLogger) Info(msg string, err ...error) {
s.logger.Info(msg, s.getWrappedError(err))
}
func (s SlogLogger) Warn(msg string, err ...error) {
s.logger.Warn(msg, s.getWrappedError(err))
}
func (s SlogLogger) Error(msg string, err ...error) {
s.logger.Error(msg, s.getWrappedError(err))
}
func (s SlogLogger) getWrappedError(err []error) slog.Attr {
var wrappedErr slog.Attr
if err != nil {
wrappedErr = slog.Any("error", wrapErr(err))
}
return wrappedErr
}

43
log/slog_test.go Normal file
View file

@ -0,0 +1,43 @@
package log
import (
"errors"
"log/slog"
"testing"
)
func Test_SlogLogger(t *testing.T) {
UseSlogAuditLogger(slog.Default())
t.Run("debug", func(t *testing.T) {
AuditLogger.Debug("debug message")
})
t.Run("debug with error details", func(t *testing.T) {
AuditLogger.Debug("debug message", errors.New("custom error"))
})
t.Run("info", func(t *testing.T) {
AuditLogger.Info("info message")
})
t.Run("info with error details", func(t *testing.T) {
AuditLogger.Info("info message", errors.New("custom error"))
})
t.Run("warn", func(t *testing.T) {
AuditLogger.Warn("warn message")
})
t.Run("warn with error details", func(t *testing.T) {
AuditLogger.Warn("warn message", errors.New("custom error"))
})
t.Run("error", func(t *testing.T) {
AuditLogger.Error("error message")
})
t.Run("error with error details", func(t *testing.T) {
AuditLogger.Error("error message", errors.New("custom error"))
})
}

25
log/zerolog.go Normal file
View file

@ -0,0 +1,25 @@
package log
import "github.com/rs/zerolog/log"
type ZeroLogLogger struct{}
func UseZerologAuditLogger() {
AuditLogger = ZeroLogLogger{}
}
func (l ZeroLogLogger) Debug(msg string, err ...error) {
log.Debug().Err(wrapErr(err)).Msg(msg)
}
func (l ZeroLogLogger) Info(msg string, err ...error) {
log.Info().Err(wrapErr(err)).Msg(msg)
}
func (l ZeroLogLogger) Warn(msg string, err ...error) {
log.Warn().Err(wrapErr(err)).Msg(msg)
}
func (l ZeroLogLogger) Error(msg string, err ...error) {
log.Error().Err(wrapErr(err)).Msg(msg)
}

42
log/zerolog_test.go Normal file
View file

@ -0,0 +1,42 @@
package log
import (
"errors"
"testing"
)
func Test_ZerologLogger(t *testing.T) {
UseZerologAuditLogger()
t.Run("debug", func(t *testing.T) {
AuditLogger.Debug("debug message")
})
t.Run("debug with error details", func(t *testing.T) {
AuditLogger.Debug("debug message", errors.New("custom error"))
})
t.Run("info", func(t *testing.T) {
AuditLogger.Info("info message")
})
t.Run("info with error details", func(t *testing.T) {
AuditLogger.Info("info message", errors.New("custom error"))
})
t.Run("warn", func(t *testing.T) {
AuditLogger.Warn("warn message")
})
t.Run("warn with error details", func(t *testing.T) {
AuditLogger.Warn("warn message", errors.New("custom error"))
})
t.Run("error", func(t *testing.T) {
AuditLogger.Error("error message")
})
t.Run("error with error details", func(t *testing.T) {
AuditLogger.Error("error message", errors.New("custom error"))
})
}

View file

@ -0,0 +1,632 @@
syntax = "proto3";
package audit.v1;
import "buf/validate/validate.proto";
import "google/protobuf/struct.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
option go_package = "./audit;auditV1";
option java_multiple_files = true;
option java_package = "com.schwarz.stackit.audit.v1";
// The audit log entry can be used to record an incident in the audit log.
message AuditLogEntry {
// The resource name of the log to which this log entry belongs.
//
// Format: <pluralType>/<identifier>/logs/<eventType>
// Where:
// Plural-Types: One from the list of supported ObjectType as plural
// Event-Types: admin-activity, system-event, policy-denied, data-access
//
// Examples:
// "projects/00b0f972-59ff-48f2-a4f9-29c57b75c2fa/logs/admin-activity"
// "billing-accounts/00b0f972-59ff-48f2-a4f9-29c57b75c2fa/logs/admin-activity"
//
// Required: true
string log_name = 1 [
(buf.validate.field).required = true,
(buf.validate.field).string.pattern = "^[a-z-]+/[a-z0-9-]+/logs/(?:admin-activity|system-event|policy-denied|data-access)$"
];
// The log entry payload, which is always an AuditLog for STACKIT Audit Log events.
//
// Required: true
AuditLog proto_payload = 2 [(buf.validate.field).required = true];
// A unique identifier for the log entry.
// Is used to check completeness of audit events over time.
//
// Format: <unix-timestamp>/<region-zone>/<worker-id>/<sequence-number>
// Where:
// Unix-Timestamp: A UTC unix timestamp in seconds is expected
// Region-Zone: The region and (optional) zone id. If both, separated with a - (dash)
// Worker-Id: The ID of the K8s Pod, Service-Instance, etc (must be unique for a sending service)
// Sequence-Number: Increasing number, representing the message offset per Worker-Id
// If the Worker-Id changes, the sequence-number has to be reset to 0.
//
// Examples:
// "1721899117/eu01/319a7fb9-edd2-46c6-953a-a724bb377c61/8792726390909855142"
// "1721899117/eu01-m/319a7fb9-edd2-46c6-953a-a724bb377c61/8792726390909855142"
//
// Required: true
string insert_id = 3 [
(buf.validate.field).required = true,
(buf.validate.field).string.pattern = "^[0-9]+/[a-z0-9-]+/[a-z0-9-]+/[0-9]+$"
];
// A set of user-defined (key, value) data that provides additional
// information about the log entry.
//
// Required: false
map<string, string> labels = 4;
// Correlate multiple audit logs by setting the same id
//
// Required: false
optional string correlation_id = 5 [
(buf.validate.field).string.min_len = 1,
(buf.validate.field).string.max_len = 255
];
// The time the event described by the log entry occurred.
//
// Required: true
google.protobuf.Timestamp timestamp = 6 [
(buf.validate.field).required = true,
(buf.validate.field).timestamp.lt_now = true
];
// The severity of the log entry.
//
// Required: true
LogSeverity severity = 7 [
(buf.validate.field).required = true,
(buf.validate.field).enum.defined_only = true
];
// Customer set W3C conform trace parent header:
// https://www.w3.org/TR/trace-context/#traceparent-header
//
// Format: <version>-<trace-id>-<parent-id>-<trace-flags>
//
// Examples:
// "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
//
// Required: false
optional string trace_parent = 8 [(buf.validate.field).string.pattern = "^[0-9]+-[a-z0-9]+-[a-z0-9]+-[0-9]+$"];
// Customer set W3C conform trace state header:
// https://www.w3.org/TR/trace-context/#tracestate-header
//
// Format: <key1>=<value1>[,<keyN>=<valueN>]
//
// Examples:
// "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"
//
// Required: false
optional string trace_state = 9;
}
// The severity of the event described in a log entry, expressed as one of the
// standard severity levels listed below.
enum LogSeverity {
LOG_SEVERITY_UNSPECIFIED = 0;
// The log entry has no assigned severity level.
LOG_SEVERITY_DEFAULT = 100;
// Debug or trace information.
LOG_SEVERITY_DEBUG = 200;
// Routine information, such as ongoing status or performance.
LOG_SEVERITY_INFO = 300;
// Normal but significant events, such as start up, shut down, or
// a configuration change.
LOG_SEVERITY_NOTICE = 400;
// Warning events might cause problems.
LOG_SEVERITY_WARNING = 500;
// Error events are likely to cause problems.
LOG_SEVERITY_ERROR = 600;
// Critical events cause more severe problems or outages.
LOG_SEVERITY_CRITICAL = 700;
// A person must take an action immediately.
LOG_SEVERITY_ALERT = 800;
// One or more systems are unusable.
LOG_SEVERITY_EMERGENCY = 900;
}
// Common audit log format for STACKIT API operations.
message AuditLog {
// The name of the API service performing the operation.
//
// Examples:
// "resource-manager"
//
// Required: true
string service_name = 1 [
(buf.validate.field).required = true,
(buf.validate.field).string.min_len = 1
];
// The name of the service method or operation.
//
// Format: stackit.<product>.<version>.<type-chain>.<operation>
// Where:
// Product: The name of the service in lowercase
// Version: Optional API version
// Type-Chain: Chained path to object
// Operation: The name of the operation in lowercase
//
// Examples:
// "stackit.resource-manager.v1.organizations.create"
// "stackit.authorization.v1.projects.volumes.create"
// "stackit.authorization.v2alpha.projects.volumes.create"
// "stackit.authorization.v2.folders.move"
// "stackit.resource-manager.health"
//
// Required: true
string operation_name = 2 [
(buf.validate.field).required = true,
(buf.validate.field).string.pattern = "^stackit\\.[a-z0-9-]+\\.(?:v[0-9]+\\.)?(?:[a-z0-9-.]+\\.)?[a-z0-9-]+$",
(buf.validate.field).string.min_len = 1,
(buf.validate.field).string.max_len = 255
];
// The resource or collection that is the target of the operation.
// The name is a scheme-less URI, not including the API service name.
//
// Format: <pluralType>/<id>[/<details>]
// Where:
// Plural-Type: One from the list of supported ObjectType as plural
// Id: The identifier of the object
// Details: Optional "<key>/<id>" pairs
//
// Examples:
// "organizations/40ab14ad-b7b0-4b1c-be41-5bc820a968d1"
// "projects/7046e7b6-5ae9-441c-99fe-2cd28a5078ec/locations/_/instances/instance-20240723-174217"
// "projects/7046e7b6-5ae9-441c-99fe-2cd28a5078ec/locations/sx-stoi01/instances/instance-20240723-174217"
// "projects/dd7d1807-54e9-4426-8994-721758b5b554/locations/eu01/vms/b6851b4e-7a9d-4973-ab0f-a80a13ee3060/ports/78f8bad4-a291-4fa3-b07f-4a1985d3dbe8"
// "projects/dd7d1807-54e9-4426-8994-721758b5b554/locations/eu01-m/vms/b6851b4e-7a9d-4973-ab0f-a80a13ee3060/ports/78f8bad4-a291-4fa3-b07f-4a1985d3dbe8"
//
// Required: true
string resource_name = 3 [
(buf.validate.field).required = true,
(buf.validate.field).string.pattern = "^[a-z]+/[a-z0-9-]+(?:/[a-z0-9-]+/[a-z0-9-_]+)*$",
(buf.validate.field).string.min_len = 1,
(buf.validate.field).string.max_len = 255
];
// Authentication information.
//
// Required: true
AuthenticationInfo authentication_info = 4 [(buf.validate.field).required = true];
// Authorization information. If there are multiple resources or permissions involved, then there is
// one AuthorizationInfo element for each {resource, permission} tuple.
//
// Required: false
repeated AuthorizationInfo authorization_info = 5;
// Metadata about the operation.
//
// Required: true
RequestMetadata request_metadata = 6 [(buf.validate.field).required = true];
// The operation request. This may not include all request parameters,
// such as those that are too large, privacy-sensitive, or duplicated
// elsewhere in the log record.
// It should never include user-generated data, such as file contents.
//
// Required: false
optional google.protobuf.Struct request = 7;
// The status of the overall operation.
//
// Required: true
ResponseMetadata response_metadata = 8 [(buf.validate.field).required = true];
// The operation response. This may not include all response elements,
// such as those that are too large, privacy-sensitive, or duplicated
// elsewhere in the log record.
//
// Required: false
optional google.protobuf.Struct response = 9;
// Other service-specific data about the request, response, and other
// information associated with the current audited event.
//
// Required: false
optional google.protobuf.Struct metadata = 10;
}
// Authentication information for the operation.
message AuthenticationInfo {
// STACKIT principal id
//
// Required: true
string principal_id = 1 [
(buf.validate.field).required = true,
(buf.validate.field).string.min_len = 1
];
// The email address of the authenticated user.
// Service accounts have email addresses that can be used.
//
// Required: true
string principal_email = 2 [
(buf.validate.field).required = true,
(buf.validate.field).string.min_len = 1,
(buf.validate.field).string.max_len = 255
];
// The name of the service account used to create or exchange
// credentials for authenticating the service account making the request.
//
// Format: projects/<id>/service-accounts/<accountId>
//
// Examples:
// "projects/29b2c56f-f712-4a9c-845b-f0907158e53c/service-accounts/a606dc68-8b97-421b-89a9-116bcbd004df"
//
// Required: false
optional string service_account_name = 3 [(buf.validate.field).string.pattern = "^[a-z-]+/[a-z0-9-]+/service-accounts/[a-z0-9-]+$"];
// Identity delegation history of an authenticated service account that makes
// the request. It contains information on the real authorities that try to
// access STACKIT resources by delegating on a service account. When multiple
// authorities present, they are guaranteed to be sorted based on the original
// ordering of the identity delegation events.
//
// Required: false
repeated ServiceAccountDelegationInfo service_account_delegation_info = 4;
}
// Authorization information for the operation.
message AuthorizationInfo {
// The resource being accessed, as a REST-style string.
//
// Format: <pluralType>/<id>[/<details>]
// Where:
// Plural-Type: One from the list of supported ObjectType as plural
// Id: The identifier of the object
// Details: Optional "<key>/<id>" pairs
//
// Examples:
// "organizations/40ab14ad-b7b0-4b1c-be41-5bc820a968d1"
// "projects/7046e7b6-5ae9-441c-99fe-2cd28a5078ec/locations/_/instances/instance-20240723-174217"
// "projects/7046e7b6-5ae9-441c-99fe-2cd28a5078ec/locations/eu01/instances/instance-20240723-174217"
// "projects/7046e7b6-5ae9-441c-99fe-2cd28a5078ec/locations/eu01/vms/b6851b4e-7a9d-4973-ab0f-a80a13ee3060/ports/78f8bad4-a291-4fa3-b07f-4a1985d3dbe8"
//
// Required: true
string resource = 1 [
(buf.validate.field).required = true,
(buf.validate.field).string.pattern = "^[a-z]+/[a-z0-9-]+(?:/[a-z0-9-]+/[a-z0-9-_]+)*$"
];
// The required IAM permission.
//
// Examples:
// "resourcemanager.project.edit"
//
// Required: false
optional string permission = 2 [(buf.validate.field).string.pattern = "^[a-z-]+(?:\\.[a-z-]+)*\\.[a-z-]+$"];
// IAM permission check result.
//
// Required: false
optional bool granted = 3;
}
// This message defines the standard attribute vocabulary for STACKIT APIs.
//
// An attribute is a piece of metadata that describes an activity on a network
// service.
message AttributeContext {
// This message defines request authentication attributes. Terminology is
// based on the JSON Web Token (JWT) standard, but the terms also
// correlate to concepts in other standards.
message Auth {
// The authenticated principal. Reflects the issuer ("iss") and subject
// ("sub") claims within a JWT.
//
// Format: <sub-claim>/<iss-claim>
// Where:
// Sub-Claim: Sub-Claim from JWT with `/` percent-encoded (url-encoded)
// Issuer-Claim: Iss-Claim from JWT with `/` percent-encoded (url-encoded)
//
// Examples:
// "stackit-resource-manager-dev/https%3A%2F%2Faccounts.dev.stackit.cloud"
//
// Required: true
string principal = 1 [
(buf.validate.field).required = true,
(buf.validate.field).string.pattern = "^[a-zA-Z0-9-%.]+/[a-zA-Z0-9-%.]+$"
];
// The intended audience(s) for this authentication information. Reflects
// the audience ("aud") claim within a JWT, typically the services intended
// to receive the credential.
//
// Examples:
// ["stackit-resource-manager-dev", "stackit", "api"]
//
// Required: false
repeated string audiences = 2;
// Structured claims presented with the credential. JWTs include
// {"key": <value>} pairs for standard and private claims.
//
// The following is a subset of the standard required and optional claims that should
// typically be presented for a STACKIT JWT.
// Don't add other claims to not leak internal or personal information:
//
// {
// "aud": "stackit-resource-manager-dev",
// "email": "max@mail.schwarz",
// "iss": "https://api.dev.stackit.cloud",
// "jti": "45a196e0-480f-4c34-a592-dc5db81c8c3a"
// "sub": "cd94f01a-df2e-4456-902f-48f5e57f0b63"
// }
//
// Required: true
google.protobuf.Struct claims = 3 [(buf.validate.field).required = true];
}
enum HttpMethod {
HTTP_METHOD_UNSPECIFIED = 0;
HTTP_METHOD_OTHER = 1;
HTTP_METHOD_GET = 2;
HTTP_METHOD_HEAD = 3;
HTTP_METHOD_POST = 4;
HTTP_METHOD_PUT = 5;
HTTP_METHOD_DELETE = 6;
HTTP_METHOD_CONNECT = 7;
HTTP_METHOD_OPTIONS = 8;
HTTP_METHOD_TRACE = 9;
HTTP_METHOD_PATCH = 10;
}
// This message defines attributes for an HTTP request. If the actual
// request is not an HTTP request, the runtime system should try to map
// the actual request to an equivalent HTTP request.
message Request {
// The unique ID for a request, which can be propagated to downstream
// systems. The ID should have low probability of collision
// within a single day for a specific service.
//
// More information can be found here: https://google.aip.dev/155
//
// Format: <idempotency-key>
// Where:
// Idempotency-key: Typically consists of a id + version
//
// Examples:
// 5e3952a9-b628-4be6-ac61-b1c6eb4a110c/5
//
// Required: false
optional string id = 1;
// The (HTTP) request method, such as `GET`, `POST`.
//
// Required: true
HttpMethod method = 2 [
(buf.validate.field).required = true,
(buf.validate.field).enum.defined_only = true
];
// The (HTTP) request headers / gRPC metadata. If multiple headers share the same key, they
// must be merged according to the HTTP spec. All header keys must be
// lowercased, because HTTP header keys are case-insensitive.
//
// Internal IP-Addresses have to be removed (e.g. in x-forwarded-xxx headers).
//
// Required: true
map<string, string> headers = 3 [(buf.validate.field).required = true];
// The gRPC / HTTP URL path.
//
// Required: true
string path = 4 [
(buf.validate.field).required = true,
(buf.validate.field).string.min_len = 1,
(buf.validate.field).string.max_len = 255
];
// The HTTP request `Host` header value.
//
// Required: true
string host = 5 [
(buf.validate.field).required = true,
(buf.validate.field).string.min_len = 1
];
// The URL scheme, such as `http`, `https` or `gRPC`.
//
// Required: true
string scheme = 6 [
(buf.validate.field).required = true,
(buf.validate.field).string.min_len = 1
];
// The HTTP URL query in the format of "name1=value1&name2=value2", as it
// appears in the first line of the HTTP request.
// The input should be escaped to not contain any special characters.
//
// Required: false
optional string query = 7;
// The timestamp when the `destination` service receives the first byte of
// the request.
//
// Required: true
google.protobuf.Timestamp time = 8 [
(buf.validate.field).required = true,
(buf.validate.field).timestamp.lt_now = true
];
// The network protocol used with the request, such as "http/1.1",
// "spdy/3", "h2", "h2c", "webrtc", "tcp", "udp", "quic". See
// https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
// for details.
//
// Required: true
string protocol = 9 [
(buf.validate.field).required = true,
(buf.validate.field).string.min_len = 1
];
// The request authentication.
//
// Required: true
Auth auth = 10 [(buf.validate.field).required = true];
}
// This message defines attributes for a typical network response. It
// generally models semantics of an HTTP response.
message Response {
// The number of items returned to the client if applicable.
//
// Required: false
optional google.protobuf.Int64Value num_response_items = 1 [(buf.validate.field).int64.gte = 0];
// The HTTP response size in bytes.
//
// Required: false
optional google.protobuf.Int64Value size = 2 [(buf.validate.field).int64.gte = 0];
// The HTTP response headers. If multiple headers share the same key, they
// must be merged according to HTTP spec. All header keys must be
// lowercased, because HTTP header keys are case-insensitive.
//
// Required: false
map<string, string> headers = 3;
// The timestamp when the "destination" service generates the first byte of
// the response.
//
// Required: true
google.protobuf.Timestamp time = 4 [
(buf.validate.field).required = true,
(buf.validate.field).timestamp.lt_now = true
];
}
}
// Metadata about the request.
message RequestMetadata {
// The IP address of the caller.
// For caller from internet, this will be public IPv4 or IPv6 address.
// For caller from a VM / K8s Service / etc, this will be the SIT proxy's IPv4 address.
//
// Required: true
string caller_ip = 1 [
(buf.validate.field).required = true,
(buf.validate.field).string.ip = true
];
// The user agent of the caller.
//
// Examples:
// "OpenAPI-Generator/1.0.0/go"
// -> The request was made by the STACKIT SDK GO client, STACKIT CLI or Terraform provider
// "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
// -> The request was made by a web browser.
//
// Required: true
string caller_supplied_user_agent = 2 [
(buf.validate.field).required = true,
(buf.validate.field).string.min_len = 1,
(buf.validate.field).string.max_len = 255
];
// This field contains request attributes like request url, time, etc.
//
// Required: true
AttributeContext.Request request_attributes = 3 [(buf.validate.field).required = true];
}
// Metadata about the response
message ResponseMetadata {
// The http or gRPC status code.
//
// Examples:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
// https://grpc.github.io/grpc/core/md_doc_statuscodes.html
//
// Required: true
google.protobuf.Int32Value status_code = 1 [
(buf.validate.field).required = true,
(buf.validate.field).int32.gte = 0
];
// Short description of the error
//
// Required: false
optional string error_message = 2;
// Error details
//
// Required: false
repeated google.protobuf.Struct error_details = 3;
// This field contains response attributes like headers, time, etc.
//
// Required: true
AttributeContext.Response response_attributes = 4 [(buf.validate.field).required = true];
}
// Identity delegation history of an authenticated service account.
message ServiceAccountDelegationInfo {
// Anonymous system principal to be used when no user identity is available.
message SystemPrincipal {
// Metadata about the service that uses the service account.
//
// Required: false
optional google.protobuf.Struct service_metadata = 1;
}
// STACKIT idp principal.
message IdpPrincipal {
// STACKIT principal id
//
// Required: true
string principal_id = 1 [
(buf.validate.field).required = true,
(buf.validate.field).string.min_len = 1
];
// The email address of the authenticated user.
// Service accounts have email addresses that can be used.
//
// Required: true
string principal_email = 2 [
(buf.validate.field).required = true,
(buf.validate.field).string.min_len = 1,
(buf.validate.field).string.max_len = 255
];
// Metadata about the service that uses the service account.
//
// Required: false
optional google.protobuf.Struct service_metadata = 3;
}
// Entity that creates credentials for service account and assumes its
// identity for authentication.
oneof authority {
option (buf.validate.oneof).required = true;
// System identity
SystemPrincipal system_principal = 1;
// STACKIT IDP identity
IdpPrincipal idp_principal = 2;
}
}

View file

@ -0,0 +1,135 @@
syntax = "proto3";
package audit.v1;
import "buf/validate/validate.proto";
option go_package = "./audit;auditV1";
option java_multiple_files = true;
option java_package = "com.schwarz.stackit.audit.v1";
enum Visibility {
VISIBILITY_UNSPECIFIED = 0;
// Will be routed to customer data sinks
VISIBILITY_PUBLIC = 1;
// Will NOT be routed to customer data sinks
VISIBILITY_PRIVATE = 2;
}
// Identifier of an object.
//
// For system events, the nil UUID must be used: 00000000-0000-0000-0000-000000000000.
message ObjectIdentifier {
// Identifier of the respective entity (e.g. Identifier of an organization)
//
// Required: true
string identifier = 1 [
(buf.validate.field).required = true,
(buf.validate.field).string.uuid = true
];
// Entity data type relevant for routing - one of the list of supported object types.
//
// Required: true
string type = 2 [
(buf.validate.field).required = true,
(buf.validate.field).string.min_len = 1
];
}
message EncryptedData {
// Encrypted serialized protobuf content (the actual audit event)
//
// Required: true
bytes data = 1 [
(buf.validate.field).required = true,
(buf.validate.field).bytes.min_len = 1
];
// Name of the protobuf type
//
// Required: true
string protobuf_type = 2 [
(buf.validate.field).required = true,
(buf.validate.field).string.min_len = 1
];
// The password taken to derive the encryption key from
//
// Required: true
string encrypted_password = 3 [
(buf.validate.field).required = true,
(buf.validate.field).string.min_len = 1
];
// Version of the encrypted key
//
// Required: true
int32 key_version = 4 [(buf.validate.field).int32.gte = 1];
}
message UnencryptedData {
// Unencrypted serialized protobuf content (the actual audit event)
//
// Required: true
bytes data = 1 [
(buf.validate.field).required = true,
(buf.validate.field).bytes.min_len = 1
];
// Name of the protobuf type
//
// Required: true
string protobuf_type = 2 [
(buf.validate.field).required = true,
(buf.validate.field).string.min_len = 1
];
}
message RoutableAuditEvent {
// Functional event name with pattern
//
// Format: stackit.<product>.<version>.<type-chain>.<operation>
// Where:
// Product: The name of the service in lowercase
// Version: Optional API version
// Type-Chain: Chained path to object
// Operation: The name of the operation in lowercase
//
// Examples:
// "stackit.resource-manager.v1.organizations.create"
// "stackit.authorization.v1.projects.volumes.create"
// "stackit.authorization.v2alpha.projects.volumes.create"
// "stackit.authorization.v2.folders.move"
// "stackit.resource-manager.health"
//
// Required: true
string operation_name = 1 [
(buf.validate.field).required = true,
(buf.validate.field).string.pattern = "^stackit\\.[a-z0-9-]+\\.(?:v[0-9]+\\.)?(?:[a-z0-9-.]+\\.)?[a-z0-9-]+$"
];
// Visibility relevant for differentiating between internal and public events
//
// Required: true
Visibility visibility = 2 [
(buf.validate.field).required = true,
(buf.validate.field).enum.defined_only = true
];
// Identifier the audit log event refers to.
//
// System events, will not be routed to the end-user.
//
// Required: true
ObjectIdentifier object_identifier = 3 [(buf.validate.field).required = true];
// The actual audit event is transferred in one of the attributes below
//
// Required: true
oneof data {
option (buf.validate.oneof).required = true;
UnencryptedData unencrypted_data = 4;
EncryptedData encrypted_data = 5;
}
}

11
proto/buf.gen.yaml Normal file
View file

@ -0,0 +1,11 @@
version: v2
plugins:
- local: protoc-gen-go
out: ../gen/go
opt:
- paths=source_relative
- local: protoc-gen-validate
out: ../gen/go
opt:
- paths=source_relative
- lang=go

8
proto/buf.lock Normal file
View file

@ -0,0 +1,8 @@
# Generated by buf. DO NOT EDIT.
version: v1
deps:
- remote: buf.build
owner: bufbuild
repository: protovalidate
commit: a6c49f84cc0f4e038680d390392e2ab0
digest: shake256:3deb629c655e469d87c58babcfbed403275a741fb4a269366c4fd6ea9db012cf562a1e64819508d73670c506f96d01f724c43bc97b44e2e02aa6e8bbdd160ab2

9
proto/buf.yaml Normal file
View file

@ -0,0 +1,9 @@
version: v1
breaking:
use:
- FILE
deps:
- buf.build/bufbuild/protovalidate
lint:
use:
- STANDARD

24
telemetry/telemetry.go Normal file
View file

@ -0,0 +1,24 @@
package telemetry
import (
"runtime/debug"
)
var AuditGoVersion = GetLibVersion("dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git")
var AuditGoGrpcVersion = GetLibVersion("dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go-grpc.git")
var AuditGoHttpVersion = GetLibVersion("dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go-http.git")
func GetLibVersion(libName string) string {
undefined := ""
bi, ok := debug.ReadBuildInfo()
if !ok {
return undefined
}
for _, dep := range bi.Deps {
if dep.Path == libName {
return dep.Version
}
}
return undefined
}