diff --git a/.azuredevops/build-pipeline.yml b/.azuredevops/build-pipeline.yml
new file mode 100644
index 0000000..965bdb7
--- /dev/null
+++ b/.azuredevops/build-pipeline.yml
@@ -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)
diff --git a/.azuredevops/release-pipeline.yml b/.azuredevops/release-pipeline.yml
new file mode 100644
index 0000000..f9360b4
--- /dev/null
+++ b/.azuredevops/release-pipeline.yml
@@ -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
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 39b72ba..3e8534f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -170,3 +170,7 @@ fabric.properties
# Editor-based Rest Client
.idea/httpRequests
+
+# Buf
+gen/java
+gen/python
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7467a01
--- /dev/null
+++ b/README.md
@@ -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
diff --git a/audit-go.iml b/audit-go.iml
new file mode 100644
index 0000000..a41966e
--- /dev/null
+++ b/audit-go.iml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/audit/api/api.go b/audit/api/api.go
new file mode 100644
index 0000000..a1d806e
--- /dev/null
+++ b/audit/api/api.go
@@ -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: ---
+ //
+ // Examples:
+ // "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
+ TraceParent *string
+
+ // Optional W3C conform trace state header:
+ // https://www.w3.org/TR/trace-context/#tracestate-header
+ //
+ // Format: =[,=]
+ //
+ // 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),
+ }
+}
diff --git a/audit/api/api_common.go b/audit/api/api_common.go
new file mode 100644
index 0000000..dc18ae3
--- /dev/null
+++ b/audit/api/api_common.go
@@ -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:
+// * Visibility: Private, ObjectIdentifier:
+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
+}
diff --git a/audit/api/api_common_test.go b/audit/api/api_common_test.go
new file mode 100644
index 0000000..c775f43
--- /dev/null
+++ b/audit/api/api_common_test.go
@@ -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)
+}
diff --git a/audit/api/api_legacy.go b/audit/api/api_legacy.go
new file mode 100644
index 0000000..f7a4375
--- /dev/null
+++ b/audit/api/api_legacy.go
@@ -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)
+}
diff --git a/audit/api/api_legacy_converter.go b/audit/api/api_legacy_converter.go
new file mode 100644
index 0000000..840c486
--- /dev/null
+++ b/audit/api/api_legacy_converter.go
@@ -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: ¶meters,
+ 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"`
+}
diff --git a/audit/api/api_legacy_converter_test.go b/audit/api/api_legacy_converter_test.go
new file mode 100644
index 0000000..769c006
--- /dev/null
+++ b/audit/api/api_legacy_converter_test.go
@@ -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)
+}
diff --git a/audit/api/api_legacy_dynamic.go b/audit/api/api_legacy_dynamic.go
new file mode 100644
index 0000000..4c04a7c
--- /dev/null
+++ b/audit/api/api_legacy_dynamic.go
@@ -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)
+}
diff --git a/audit/api/api_legacy_dynamic_test.go b/audit/api/api_legacy_dynamic_test.go
new file mode 100644
index 0000000..3c6f3c0
--- /dev/null
+++ b/audit/api/api_legacy_dynamic_test.go
@@ -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)
+}
diff --git a/audit/api/api_legacy_test.go b/audit/api/api_legacy_test.go
new file mode 100644
index 0000000..379975c
--- /dev/null
+++ b/audit/api/api_legacy_test.go
@@ -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)
+}
diff --git a/audit/api/api_mock.go b/audit/api/api_mock.go
new file mode 100644
index 0000000..7cb9604
--- /dev/null
+++ b/audit/api/api_mock.go
@@ -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
+}
diff --git a/audit/api/api_mock_test.go b/audit/api/api_mock_test.go
new file mode 100644
index 0000000..1cb90d0
--- /dev/null
+++ b/audit/api/api_mock_test.go
@@ -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))
+ })
+}
diff --git a/audit/api/api_routable.go b/audit/api/api_routable.go
new file mode 100644
index 0000000..7d05836
--- /dev/null
+++ b/audit/api/api_routable.go
@@ -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)
+}
diff --git a/audit/api/api_routable_test.go b/audit/api/api_routable_test.go
new file mode 100644
index 0000000..e942f09
--- /dev/null
+++ b/audit/api/api_routable_test.go
@@ -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)
+}
diff --git a/audit/api/base64.go b/audit/api/base64.go
new file mode 100644
index 0000000..a2d7b4a
--- /dev/null
+++ b/audit/api/base64.go
@@ -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
+}
diff --git a/audit/api/base64_test.go b/audit/api/base64_test.go
new file mode 100644
index 0000000..321225f
--- /dev/null
+++ b/audit/api/base64_test.go
@@ -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)
+ })
+}
diff --git a/audit/api/builder.go b/audit/api/builder.go
new file mode 100644
index 0000000..b4fd39e
--- /dev/null
+++ b/audit/api/builder.go
@@ -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....
+// 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....
+// 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
+}
diff --git a/audit/api/builder_test.go b/audit/api/builder_test.go
new file mode 100644
index 0000000..f2a4a9b
--- /dev/null
+++ b/audit/api/builder_test.go
@@ -0,0 +1,1167 @@
+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"
+ "fmt"
+ "github.com/bufbuild/protovalidate-go"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "go.opentelemetry.io/otel"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/types/known/structpb"
+ "google.golang.org/protobuf/types/known/wrapperspb"
+ "testing"
+ "time"
+)
+
+func Test_getObjectIdAndTypeFromAuditParams(t *testing.T) {
+
+ t.Run(
+ "object id empty", func(t *testing.T) {
+ objectId, objectType, err := getObjectIdAndTypeFromAuditParams(&AuditParameters{})
+ assert.EqualError(t, err, "object id missing")
+ assert.Equal(t, "", objectId)
+ assert.Nil(t, objectType)
+ },
+ )
+
+ t.Run(
+ "object type empty", func(t *testing.T) {
+ objectId, objectType, err := getObjectIdAndTypeFromAuditParams(&AuditParameters{ObjectId: "value"})
+ assert.EqualError(t, err, "object type missing")
+ assert.Equal(t, "", objectId)
+ assert.Nil(t, objectType)
+ },
+ )
+
+ t.Run(
+ "object id and invalid type set", func(t *testing.T) {
+ objectId, objectType, err := getObjectIdAndTypeFromAuditParams(
+ &AuditParameters{
+ ObjectId: "value",
+ ObjectType: ObjectTypeFromPluralString("invalid"),
+ },
+ )
+ assert.EqualError(t, err, "unknown object type")
+ assert.Equal(t, "", objectId)
+ assert.Nil(t, objectType)
+ },
+ )
+
+ t.Run(
+ "object id and type set", func(t *testing.T) {
+ objectId, objectType, err := getObjectIdAndTypeFromAuditParams(
+ &AuditParameters{
+ ObjectId: "value",
+ ObjectType: ObjectTypeProject,
+ },
+ )
+ assert.NoError(t, err)
+ assert.Equal(t, "value", objectId)
+ assert.Equal(t, ObjectTypeProject, *objectType)
+ },
+ )
+}
+
+func Test_AuditLogEntryBuilder(t *testing.T) {
+
+ t.Run("nothing set", func(t *testing.T) {
+ logEntry, err := NewAuditLogEntryBuilder().Build(context.Background(), SequenceNumber(1))
+ assert.Error(t, err)
+ assert.Equal(t, "object id missing", err.Error())
+ assert.Nil(t, logEntry)
+ })
+
+ t.Run("details missing", func(t *testing.T) {
+ logEntry, err := NewAuditLogEntryBuilder().WithRequiredLocation("eu01").
+ WithRequiredObjectId("1").
+ WithRequiredObjectType(ObjectTypeProject).
+ Build(context.Background(), SequenceNumber(1))
+
+ assert.NoError(t, err)
+ assert.NotNil(t, logEntry)
+
+ validator, err := protovalidate.New()
+ assert.NoError(t, err)
+ err = validator.Validate(logEntry)
+ assert.Error(t, err)
+ assert.Equal(t, "validation error:\n - proto_payload.service_name: value is required [required]\n - proto_payload.operation_name: value is required [required]\n - proto_payload.request_metadata.caller_supplied_user_agent: value is required [required]\n - proto_payload.request_metadata.request_attributes.method: value is required [required]\n - proto_payload.request_metadata.request_attributes.headers: value is required [required]\n - proto_payload.request_metadata.request_attributes.path: value is required [required]\n - proto_payload.request_metadata.request_attributes.host: value is required [required]\n - proto_payload.request_metadata.request_attributes.scheme: value is required [required]\n - proto_payload.request_metadata.request_attributes.protocol: value is required [required]\n - insert_id: value does not match regex pattern `^[0-9]+/[a-z0-9-]+/[a-z0-9-]+/[0-9]+$` [string.pattern]", err.Error())
+ })
+
+ t.Run("required only", func(t *testing.T) {
+ builder := NewAuditLogEntryBuilder().
+ WithRequiredLocation("eu01").
+ WithRequiredObjectId("1").
+ WithRequiredObjectType(ObjectTypeProject).
+ WithRequiredOperation("stackit.demo-service.v1.operation").
+ WithRequiredApiRequest(ApiRequest{
+ Body: nil,
+ Header: TestHeaders,
+ Host: "localhost",
+ Method: "POST",
+ Scheme: "https",
+ Proto: "HTTP/1.1",
+ URL: RequestUrl{
+ Path: "/",
+ RawQuery: nil,
+ },
+ }).
+ WithRequiredRequestClientIp("127.0.0.1").
+ WithRequiredServiceName("demo-service").
+ WithRequiredWorkerId("worker-id")
+
+ logEntry, err := builder.Build(context.Background(), SequenceNumber(1))
+ assert.NoError(t, err)
+ assert.NotNil(t, logEntry)
+
+ assert.Equal(t, "projects/1/logs/admin-activity", logEntry.LogName)
+ assert.Nil(t, logEntry.Labels)
+ assert.Nil(t, logEntry.TraceState)
+ assert.Nil(t, logEntry.TraceParent)
+ assert.Equal(t, auditV1.LogSeverity_LOG_SEVERITY_DEFAULT, logEntry.Severity)
+ assert.NotNil(t, logEntry.Timestamp)
+ assert.Nil(t, logEntry.CorrelationId)
+ assert.Regexp(t, "[0-9]+/eu01/worker-id/1", logEntry.InsertId)
+
+ assert.NotNil(t, logEntry.ProtoPayload)
+
+ authenticationInfo := logEntry.ProtoPayload.AuthenticationInfo
+ assert.NotNil(t, authenticationInfo)
+ assert.Equal(t, "Christian.Schaible@novatec-gmbh.de", authenticationInfo.PrincipalEmail)
+ assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", authenticationInfo.PrincipalId)
+ assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
+ assert.Nil(t, authenticationInfo.ServiceAccountName)
+
+ assert.Nil(t, logEntry.ProtoPayload.AuthorizationInfo)
+ assert.Nil(t, logEntry.ProtoPayload.Metadata)
+ assert.Equal(t, "stackit.demo-service.v1.operation", logEntry.ProtoPayload.OperationName)
+ assert.Nil(t, logEntry.ProtoPayload.Request)
+
+ requestMetadata := logEntry.ProtoPayload.RequestMetadata
+ assert.NotNil(t, requestMetadata)
+ assert.Equal(t, "127.0.0.1", requestMetadata.CallerIp)
+ assert.Equal(t, "custom", requestMetadata.CallerSuppliedUserAgent)
+
+ requestAttributes := requestMetadata.RequestAttributes
+ assert.NotNil(t, requestAttributes)
+ assert.Equal(t, "/", requestAttributes.Path)
+ assert.NotNil(t, requestAttributes.Time)
+ assert.Equal(t, "localhost", requestAttributes.Host)
+ assert.Equal(t, auditV1.AttributeContext_HTTP_METHOD_POST, requestAttributes.Method)
+ assert.Nil(t, requestAttributes.Id)
+ assert.Equal(t, "https", requestAttributes.Scheme)
+ assert.Equal(t, map[string]string{"user-agent": "custom"}, requestAttributes.Headers)
+ assert.Nil(t, requestAttributes.Query)
+ assert.Equal(t, "HTTP/1.1", requestAttributes.Protocol)
+
+ requestAttributesAuth := requestAttributes.Auth
+ assert.NotNil(t, requestAttributesAuth)
+ assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63/https%3A%2F%2Faccounts.dev.stackit.cloud", requestAttributesAuth.Principal)
+ assert.Equal(t, []string{"stackit-portal-login-dev-client-id"}, requestAttributesAuth.Audiences)
+ assert.NotNil(t, requestAttributesAuth.Claims)
+
+ assert.Equal(t, "projects/1", logEntry.ProtoPayload.ResourceName)
+ assert.Nil(t, logEntry.ProtoPayload.Response)
+
+ responseMetadata := logEntry.ProtoPayload.ResponseMetadata
+ assert.NotNil(t, responseMetadata)
+ assert.Nil(t, responseMetadata.ErrorDetails)
+ assert.Nil(t, responseMetadata.ErrorMessage)
+ assert.Equal(t, wrapperspb.Int32(200), responseMetadata.StatusCode)
+
+ responseAttributes := responseMetadata.ResponseAttributes
+ assert.NotNil(t, responseAttributes)
+ assert.Nil(t, responseAttributes.Headers)
+ assert.Nil(t, responseAttributes.NumResponseItems)
+ assert.Nil(t, responseAttributes.Size)
+ assert.NotNil(t, responseAttributes.Time)
+
+ assert.Equal(t, "demo-service", logEntry.ProtoPayload.ServiceName)
+
+ validator, err := protovalidate.New()
+ assert.NoError(t, err)
+ err = validator.Validate(logEntry)
+ assert.NoError(t, err)
+ })
+
+ t.Run("with details", func(t *testing.T) {
+ details := map[string]interface{}{"key": "detail"}
+ permission := "project.edit"
+ permissionCheckResult := true
+ requestTime := time.Now().AddDate(0, 0, -2).UTC()
+ responseTime := time.Now().AddDate(0, 0, -1).UTC()
+ responseBody := map[string]interface{}{"key": "response"}
+ responseBodyBytes, err := ResponseBodyToBytes(responseBody)
+ assert.NoError(t, err)
+ builder := NewAuditLogEntryBuilder().
+ WithRequiredLocation("eu01").
+ WithRequiredObjectId("1").
+ WithRequiredObjectType(ObjectTypeProject).
+ WithRequiredOperation("stackit.demo-service.v1.operation").
+ WithRequiredApiRequest(ApiRequest{
+ Body: nil,
+ Header: TestHeaders,
+ Host: "localhost",
+ Method: "POST",
+ Scheme: "https",
+ Proto: "HTTP/1.1",
+ URL: RequestUrl{
+ Path: "/",
+ RawQuery: nil,
+ },
+ }).
+ WithRequiredRequestClientIp("127.0.0.1").
+ WithRequiredServiceName("demo-service").
+ WithRequiredWorkerId("worker-id").
+ WithAuditPermission(permission).
+ WithAuditPermissionCheckResult(permissionCheckResult).
+ WithDetails(details).
+ WithEventType(EventTypePolicyDenied).
+ WithLabels(map[string]string{"key": "label"}).
+ WithNumResponseItems(int64(10)).
+ WithRequestCorrelationId("correlationId").
+ WithRequestId("requestId").
+ WithRequestTime(requestTime).
+ WithResponseBodyBytes(responseBodyBytes).
+ WithResponseHeaders(map[string][]string{"key": {"header"}}).
+ WithResponseTime(responseTime).
+ WithSeverity(auditV1.LogSeverity_LOG_SEVERITY_ERROR).
+ WithStatusCode(400)
+
+ logEntry, err := builder.Build(context.Background(), SequenceNumber(1))
+ assert.NoError(t, err)
+ assert.NotNil(t, logEntry)
+
+ assert.Equal(t, "projects/1/logs/policy-denied", logEntry.LogName)
+ assert.Equal(t, map[string]string{"key": "label"}, logEntry.Labels)
+ assert.Nil(t, logEntry.TraceState)
+ assert.Nil(t, logEntry.TraceParent)
+ assert.Equal(t, auditV1.LogSeverity_LOG_SEVERITY_ERROR, logEntry.Severity)
+ assert.NotNil(t, logEntry.Timestamp)
+ assert.Equal(t, "correlationId", *logEntry.CorrelationId)
+ assert.Regexp(t, "[0-9]+/eu01/worker-id/1", logEntry.InsertId)
+
+ assert.NotNil(t, logEntry.ProtoPayload)
+
+ authenticationInfo := logEntry.ProtoPayload.AuthenticationInfo
+ assert.NotNil(t, authenticationInfo)
+ assert.Equal(t, "Christian.Schaible@novatec-gmbh.de", authenticationInfo.PrincipalEmail)
+ assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", authenticationInfo.PrincipalId)
+ assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
+ assert.Nil(t, authenticationInfo.ServiceAccountName)
+
+ assert.Equal(t, []*auditV1.AuthorizationInfo{{
+ Resource: "projects/1",
+ Permission: &permission,
+ Granted: &permissionCheckResult,
+ }}, logEntry.ProtoPayload.AuthorizationInfo)
+
+ expectedMetadata, _ := structpb.NewStruct(details)
+ assert.Equal(t, expectedMetadata, logEntry.ProtoPayload.Metadata)
+ assert.Equal(t, "stackit.demo-service.v1.operation", logEntry.ProtoPayload.OperationName)
+ assert.Nil(t, logEntry.ProtoPayload.Request)
+
+ requestMetadata := logEntry.ProtoPayload.RequestMetadata
+ assert.NotNil(t, requestMetadata)
+ assert.Equal(t, "127.0.0.1", requestMetadata.CallerIp)
+ assert.Equal(t, "custom", requestMetadata.CallerSuppliedUserAgent)
+
+ requestAttributes := requestMetadata.RequestAttributes
+ assert.NotNil(t, requestAttributes)
+ assert.Equal(t, "/", requestAttributes.Path)
+ assert.NotNil(t, requestAttributes.Time)
+ assert.Equal(t, "localhost", requestAttributes.Host)
+ assert.Equal(t, auditV1.AttributeContext_HTTP_METHOD_POST, requestAttributes.Method)
+ assert.Equal(t, "requestId", *requestAttributes.Id)
+ assert.Equal(t, "https", requestAttributes.Scheme)
+ assert.Equal(t, map[string]string{"user-agent": "custom"}, requestAttributes.Headers)
+ assert.Nil(t, requestAttributes.Query)
+ assert.Equal(t, "HTTP/1.1", requestAttributes.Protocol)
+
+ requestAttributesAuth := requestAttributes.Auth
+ assert.NotNil(t, requestAttributesAuth)
+ assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63/https%3A%2F%2Faccounts.dev.stackit.cloud", requestAttributesAuth.Principal)
+ assert.Equal(t, []string{"stackit-portal-login-dev-client-id"}, requestAttributesAuth.Audiences)
+ assert.NotNil(t, requestAttributesAuth.Claims)
+
+ expectedResponse, _ := structpb.NewStruct(responseBody)
+ assert.Equal(t, "projects/1", logEntry.ProtoPayload.ResourceName)
+ assert.Equal(t, expectedResponse, logEntry.ProtoPayload.Response)
+
+ responseMetadata := logEntry.ProtoPayload.ResponseMetadata
+ assert.NotNil(t, responseMetadata)
+ assert.Nil(t, responseMetadata.ErrorDetails)
+ assert.Equal(t, "Client error", *responseMetadata.ErrorMessage)
+ assert.Equal(t, wrapperspb.Int32(400), responseMetadata.StatusCode)
+
+ responseAttributes := responseMetadata.ResponseAttributes
+ assert.NotNil(t, responseAttributes)
+ assert.Equal(t, map[string]string{"key": "header"}, responseAttributes.Headers)
+ assert.Equal(t, wrapperspb.Int64(10), responseAttributes.NumResponseItems)
+ assert.Equal(t, wrapperspb.Int64(18), responseAttributes.Size)
+ assert.NotNil(t, responseAttributes.Time)
+
+ assert.Equal(t, "demo-service", logEntry.ProtoPayload.ServiceName)
+
+ validator, err := protovalidate.New()
+ assert.NoError(t, err)
+ err = validator.Validate(logEntry)
+ assert.NoError(t, err)
+ })
+
+ t.Run("system event", func(t *testing.T) {
+ builder := NewAuditLogEntryBuilder().
+ WithRequiredLocation("eu01").
+ WithRequiredObjectId("1").
+ WithRequiredObjectType(ObjectTypeProject).
+ WithRequiredOperation("stackit.demo-service.v1.operation").
+ WithRequiredServiceName("demo-service").
+ WithRequiredWorkerId("worker-id").
+ AsSystemEvent()
+
+ logEntry, err := builder.Build(context.Background(), SequenceNumber(1))
+ assert.NoError(t, err)
+ assert.NotNil(t, logEntry)
+
+ assert.Equal(t, fmt.Sprintf("system/%s/logs/system-event", uuid.Nil.String()), logEntry.LogName)
+ assert.Nil(t, logEntry.Labels)
+ assert.Nil(t, logEntry.TraceState)
+ assert.Nil(t, logEntry.TraceParent)
+ assert.Equal(t, auditV1.LogSeverity_LOG_SEVERITY_DEFAULT, logEntry.Severity)
+ assert.NotNil(t, logEntry.Timestamp)
+ assert.Nil(t, logEntry.CorrelationId)
+ assert.Regexp(t, "[0-9]+/eu01/worker-id/1", logEntry.InsertId)
+
+ assert.NotNil(t, logEntry.ProtoPayload)
+
+ authenticationInfo := logEntry.ProtoPayload.AuthenticationInfo
+ assert.NotNil(t, authenticationInfo)
+ assert.Equal(t, "do-not-reply@stackit.cloud", authenticationInfo.PrincipalEmail)
+ assert.Equal(t, "none", authenticationInfo.PrincipalId)
+ assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
+ assert.Nil(t, authenticationInfo.ServiceAccountName)
+
+ assert.Nil(t, logEntry.ProtoPayload.AuthorizationInfo)
+ assert.Nil(t, logEntry.ProtoPayload.Metadata)
+ assert.Equal(t, "stackit.demo-service.v1.operation", logEntry.ProtoPayload.OperationName)
+ assert.Nil(t, logEntry.ProtoPayload.Request)
+
+ requestMetadata := logEntry.ProtoPayload.RequestMetadata
+ assert.NotNil(t, requestMetadata)
+ assert.Equal(t, "0.0.0.0", requestMetadata.CallerIp)
+ assert.Equal(t, "none", requestMetadata.CallerSuppliedUserAgent)
+
+ requestAttributes := requestMetadata.RequestAttributes
+ assert.NotNil(t, requestAttributes)
+ assert.Equal(t, "none", requestAttributes.Path)
+ assert.NotNil(t, requestAttributes.Time)
+ assert.Equal(t, "0.0.0.0", requestAttributes.Host)
+ assert.Equal(t, auditV1.AttributeContext_HTTP_METHOD_OTHER, requestAttributes.Method)
+ assert.Nil(t, requestAttributes.Id)
+ assert.Equal(t, "none", requestAttributes.Scheme)
+ assert.Equal(t, map[string]string{"user-agent": "none"}, requestAttributes.Headers)
+ assert.Nil(t, requestAttributes.Query)
+ assert.Equal(t, "none", requestAttributes.Protocol)
+
+ requestAttributesAuth := requestAttributes.Auth
+ assert.NotNil(t, requestAttributesAuth)
+ assert.Equal(t, "none/none", requestAttributesAuth.Principal)
+ assert.Equal(t, []string{}, requestAttributesAuth.Audiences)
+ assert.NotNil(t, requestAttributesAuth.Claims)
+ assert.Equal(t, map[string]any{}, requestAttributesAuth.Claims.AsMap())
+
+ assert.Equal(t, "projects/1", logEntry.ProtoPayload.ResourceName)
+ assert.Nil(t, logEntry.ProtoPayload.Response)
+
+ responseMetadata := logEntry.ProtoPayload.ResponseMetadata
+ assert.NotNil(t, responseMetadata)
+ assert.Nil(t, responseMetadata.ErrorDetails)
+ assert.Nil(t, responseMetadata.ErrorMessage)
+ assert.Equal(t, wrapperspb.Int32(200), responseMetadata.StatusCode)
+
+ responseAttributes := responseMetadata.ResponseAttributes
+ assert.NotNil(t, responseAttributes)
+ assert.Nil(t, responseAttributes.Headers)
+ assert.Nil(t, responseAttributes.NumResponseItems)
+ assert.Nil(t, responseAttributes.Size)
+ assert.NotNil(t, responseAttributes.Time)
+
+ assert.Equal(t, "demo-service", logEntry.ProtoPayload.ServiceName)
+
+ validator, err := protovalidate.New()
+ assert.NoError(t, err)
+ err = validator.Validate(logEntry)
+ assert.NoError(t, err)
+ })
+
+ t.Run("with response body unserialized", func(t *testing.T) {
+ details := map[string]interface{}{"key": "detail"}
+ permission := "project.edit"
+ permissionCheckResult := true
+ requestTime := time.Now().AddDate(0, 0, -2).UTC()
+ responseTime := time.Now().AddDate(0, 0, -1).UTC()
+ responseBody := map[string]interface{}{"key": "response"}
+ builder := NewAuditLogEntryBuilder().
+ WithRequiredLocation("eu01").
+ WithRequiredObjectId("1").
+ WithRequiredObjectType(ObjectTypeProject).
+ WithRequiredOperation("stackit.demo-service.v1.operation").
+ WithRequiredApiRequest(ApiRequest{
+ Body: nil,
+ Header: TestHeaders,
+ Host: "localhost",
+ Method: "POST",
+ Scheme: "https",
+ Proto: "HTTP/1.1",
+ URL: RequestUrl{
+ Path: "/",
+ RawQuery: nil,
+ },
+ }).
+ WithRequiredRequestClientIp("127.0.0.1").
+ WithRequiredServiceName("demo-service").
+ WithRequiredWorkerId("worker-id").
+ WithAuditPermission(permission).
+ WithAuditPermissionCheckResult(permissionCheckResult).
+ WithDetails(details).
+ WithEventType(EventTypeSystemEvent).
+ WithLabels(map[string]string{"key": "label"}).
+ WithNumResponseItems(int64(10)).
+ WithRequestCorrelationId("correlationId").
+ WithRequestId("requestId").
+ WithRequestTime(requestTime).
+ WithResponseBody(responseBody).
+ WithResponseHeaders(map[string][]string{"key": {"header"}}).
+ WithResponseTime(responseTime).
+ WithSeverity(auditV1.LogSeverity_LOG_SEVERITY_ERROR).
+ WithStatusCode(400)
+
+ logEntry, err := builder.Build(context.Background(), SequenceNumber(1))
+ assert.NoError(t, err)
+ assert.NotNil(t, logEntry)
+ assert.NotNil(t, logEntry.ProtoPayload)
+
+ expectedResponse, _ := structpb.NewStruct(responseBody)
+ assert.Equal(t, "projects/1", logEntry.ProtoPayload.ResourceName)
+ assert.Equal(t, expectedResponse, logEntry.ProtoPayload.Response)
+
+ validator, err := protovalidate.New()
+ assert.NoError(t, err)
+ err = validator.Validate(logEntry)
+ assert.NoError(t, err)
+ })
+
+ t.Run("with response body and response body bytes set", func(t *testing.T) {
+ responseBody := map[string]interface{}{"key": "response"}
+ responseBodyBytes, err := ResponseBodyToBytes(responseBody)
+ assert.NoError(t, err)
+ builder := NewAuditLogEntryBuilder().
+ WithRequiredApiRequest(ApiRequest{
+ Body: nil,
+ Header: TestHeaders,
+ Host: "localhost",
+ Method: "POST",
+ Scheme: "https",
+ Proto: "HTTP/1.1",
+ URL: RequestUrl{
+ Path: "/",
+ RawQuery: nil,
+ },
+ }).
+ WithRequiredLocation("eu01").
+ WithRequiredObjectId("1").
+ WithRequiredObjectType(ObjectTypeProject).
+ WithRequiredOperation("stackit.demo-service.v1.operation").
+ WithRequiredRequestClientIp("127.0.0.1").
+ WithRequiredServiceName("demo-service").
+ WithRequiredWorkerId("worker-id").
+ WithResponseBody(responseBody).
+ WithResponseBodyBytes(responseBodyBytes)
+
+ logEntry, err := builder.Build(context.Background(), SequenceNumber(1))
+ assert.EqualError(t, err, "responseBodyBytes and responseBody set")
+ assert.Nil(t, logEntry)
+ })
+
+ t.Run("with invalid response body", func(t *testing.T) {
+ builder := NewAuditLogEntryBuilder().
+ WithRequiredApiRequest(ApiRequest{
+ Body: nil,
+ Header: TestHeaders,
+ Host: "localhost",
+ Method: "POST",
+ Scheme: "https",
+ Proto: "HTTP/1.1",
+ URL: RequestUrl{
+ Path: "/",
+ RawQuery: nil,
+ },
+ }).
+ WithRequiredLocation("eu01").
+ WithRequiredObjectId("1").
+ WithRequiredObjectType(ObjectTypeProject).
+ WithRequiredOperation("stackit.demo-service.v1.operation").
+ WithRequiredRequestClientIp("127.0.0.1").
+ WithRequiredServiceName("demo-service").
+ WithRequiredWorkerId("worker-id").
+ WithResponseBody("invalid")
+
+ logEntry, err := builder.Build(context.Background(), SequenceNumber(1))
+ assert.EqualError(t, err, "json: cannot unmarshal string into Go value of type map[string]interface {}\ninvalid response")
+ assert.Nil(t, logEntry)
+ })
+}
+
+func Test_AuditEventBuilder(t *testing.T) {
+
+ t.Run("nothing set", func(t *testing.T) {
+ api, _ := NewMockAuditApi()
+ sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator()
+ tracer := otel.Tracer("test")
+
+ cloudEvent, routingIdentifier, err := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", "worker-id", "eu01").
+ Build(context.Background(), SequenceNumber(1))
+
+ assert.Error(t, err)
+ assert.Equal(t, "object id missing", err.Error())
+ assert.Nil(t, cloudEvent)
+ assert.Nil(t, routingIdentifier)
+ })
+
+ t.Run("details missing", func(t *testing.T) {
+ api, _ := NewMockAuditApi()
+ sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator()
+ tracer := otel.Tracer("test")
+
+ cloudEvent, routingIdentifier, err := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", "worker-id", "eu01").
+ WithRequiredObjectId("objectId").
+ WithRequiredObjectType(ObjectTypeProject).
+ Build(context.Background(), SequenceNumber(1))
+
+ assert.Error(t, err)
+ assert.Equal(t, "validation error:\n - log_name: value does not match regex pattern `^[a-z-]+/[a-z0-9-]+/logs/(?:admin-activity|system-event|policy-denied|data-access)$` [string.pattern]\n - proto_payload.operation_name: value is required [required]\n - proto_payload.resource_name: value does not match regex pattern `^[a-z]+/[a-z0-9-]+(?:/[a-z0-9-]+/[a-z0-9-_]+)*$` [string.pattern]\n - proto_payload.request_metadata.caller_supplied_user_agent: value is required [required]\n - proto_payload.request_metadata.request_attributes.method: value is required [required]\n - proto_payload.request_metadata.request_attributes.headers: value is required [required]\n - proto_payload.request_metadata.request_attributes.path: value is required [required]\n - proto_payload.request_metadata.request_attributes.host: value is required [required]\n - proto_payload.request_metadata.request_attributes.scheme: value is required [required]\n - proto_payload.request_metadata.request_attributes.protocol: value is required [required]", err.Error())
+ assert.Nil(t, cloudEvent)
+ assert.Nil(t, routingIdentifier)
+ })
+
+ t.Run("required only", func(t *testing.T) {
+ api, _ := NewMockAuditApi()
+ sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator()
+ tracer := otel.Tracer("test")
+
+ objectId := uuid.NewString()
+ operation := "stackit.demo-service.v1.operation"
+ builder := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", "worker-id", "eu01").
+ WithRequiredObjectId(objectId).
+ WithRequiredObjectType(ObjectTypeProject).
+ WithRequiredOperation(operation).
+ WithRequiredApiRequest(ApiRequest{
+ Body: nil,
+ Header: TestHeaders,
+ Host: "localhost",
+ Method: "POST",
+ Scheme: "https",
+ Proto: "HTTP/1.1",
+ URL: RequestUrl{
+ Path: "/",
+ RawQuery: nil,
+ },
+ }).
+ WithRequiredRequestClientIp("127.0.0.1")
+
+ routableIdentifier := RoutableIdentifier{Identifier: objectId, Type: ObjectTypeProject}
+
+ cloudEvent, routingIdentifier, err := builder.Build(context.Background(), SequenceNumber(1))
+ assert.NoError(t, err)
+ assert.True(t, builder.IsBuilt())
+
+ assert.Equal(t, &routableIdentifier, routingIdentifier)
+
+ assert.NotNil(t, cloudEvent)
+ assert.Equal(t, "application/cloudevents+protobuf", cloudEvent.DataContentType)
+ assert.Equal(t, "audit.v1.RoutableAuditEvent", cloudEvent.DataType)
+ assert.Regexp(t, "[0-9]+/eu01/worker-id/1", cloudEvent.Id)
+ assert.Equal(t, "demo-service", cloudEvent.Source)
+ assert.Equal(t, "1.0", cloudEvent.SpecVersion)
+ assert.Equal(t, fmt.Sprintf("projects/%s", objectId), cloudEvent.Subject)
+ assert.NotNil(t, cloudEvent.Time)
+ assert.Equal(t, "00-00000000000000000000000000000000-0000000000000000-00", *cloudEvent.TraceParent)
+ assert.Nil(t, cloudEvent.TraceState)
+
+ var routableAuditEvent auditV1.RoutableAuditEvent
+ assert.NotNil(t, cloudEvent.Data)
+ assert.NoError(t, proto.Unmarshal(cloudEvent.Data, &routableAuditEvent))
+
+ assert.Equal(t, routableIdentifier.ToObjectIdentifier().Identifier, routableAuditEvent.ObjectIdentifier.Identifier)
+ assert.Equal(t, routableIdentifier.ToObjectIdentifier().Type, routableAuditEvent.ObjectIdentifier.Type)
+ assert.Equal(t, auditV1.Visibility_VISIBILITY_PUBLIC, routableAuditEvent.Visibility)
+ assert.Equal(t, operation, routableAuditEvent.OperationName)
+
+ var logEntry auditV1.AuditLogEntry
+ assert.NotNil(t, routableAuditEvent.GetUnencryptedData().Data)
+ assert.NoError(t, proto.Unmarshal(routableAuditEvent.GetUnencryptedData().Data, &logEntry))
+
+ assert.Equal(t, fmt.Sprintf("projects/%s/logs/admin-activity", objectId), logEntry.LogName)
+ assert.Nil(t, logEntry.Labels)
+ assert.Nil(t, logEntry.TraceState)
+ assert.Nil(t, logEntry.TraceParent)
+ assert.Equal(t, auditV1.LogSeverity_LOG_SEVERITY_DEFAULT, logEntry.Severity)
+ assert.NotNil(t, logEntry.Timestamp)
+ assert.Nil(t, logEntry.CorrelationId)
+ assert.Regexp(t, "[0-9]+/eu01/worker-id/1", logEntry.InsertId)
+
+ assert.NotNil(t, logEntry.ProtoPayload)
+
+ authenticationInfo := logEntry.ProtoPayload.AuthenticationInfo
+ assert.NotNil(t, authenticationInfo)
+ assert.Equal(t, "Christian.Schaible@novatec-gmbh.de", authenticationInfo.PrincipalEmail)
+ assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", authenticationInfo.PrincipalId)
+ assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
+ assert.Nil(t, authenticationInfo.ServiceAccountName)
+
+ assert.Nil(t, logEntry.ProtoPayload.AuthorizationInfo)
+ assert.Nil(t, logEntry.ProtoPayload.Metadata)
+ assert.Equal(t, operation, logEntry.ProtoPayload.OperationName)
+ assert.Nil(t, logEntry.ProtoPayload.Request)
+
+ requestMetadata := logEntry.ProtoPayload.RequestMetadata
+ assert.NotNil(t, requestMetadata)
+ assert.Equal(t, "127.0.0.1", requestMetadata.CallerIp)
+ assert.Equal(t, "custom", requestMetadata.CallerSuppliedUserAgent)
+
+ requestAttributes := requestMetadata.RequestAttributes
+ assert.NotNil(t, requestAttributes)
+ assert.Equal(t, "/", requestAttributes.Path)
+ assert.NotNil(t, requestAttributes.Time)
+ assert.Equal(t, "localhost", requestAttributes.Host)
+ assert.Equal(t, auditV1.AttributeContext_HTTP_METHOD_POST, requestAttributes.Method)
+ assert.Nil(t, requestAttributes.Id)
+ assert.Equal(t, "https", requestAttributes.Scheme)
+ assert.Equal(t, map[string]string{"user-agent": "custom"}, requestAttributes.Headers)
+ assert.Nil(t, requestAttributes.Query)
+ assert.Equal(t, "HTTP/1.1", requestAttributes.Protocol)
+
+ requestAttributesAuth := requestAttributes.Auth
+ assert.NotNil(t, requestAttributesAuth)
+ assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63/https%3A%2F%2Faccounts.dev.stackit.cloud", requestAttributesAuth.Principal)
+ assert.Equal(t, []string{"stackit-portal-login-dev-client-id"}, requestAttributesAuth.Audiences)
+ assert.NotNil(t, requestAttributesAuth.Claims)
+
+ assert.Equal(t, fmt.Sprintf("projects/%s", objectId), logEntry.ProtoPayload.ResourceName)
+ assert.Nil(t, logEntry.ProtoPayload.Response)
+
+ responseMetadata := logEntry.ProtoPayload.ResponseMetadata
+ assert.NotNil(t, responseMetadata)
+ assert.Nil(t, responseMetadata.ErrorDetails)
+ assert.Nil(t, responseMetadata.ErrorMessage)
+ assert.Equal(t, wrapperspb.Int32(200), responseMetadata.StatusCode)
+
+ responseAttributes := responseMetadata.ResponseAttributes
+ assert.NotNil(t, responseAttributes)
+ assert.Nil(t, responseAttributes.Headers)
+ assert.Nil(t, responseAttributes.NumResponseItems)
+ assert.Nil(t, responseAttributes.Size)
+ assert.NotNil(t, responseAttributes.Time)
+
+ assert.Equal(t, "demo-service", logEntry.ProtoPayload.ServiceName)
+
+ validator, err := protovalidate.New()
+ assert.NoError(t, err)
+ err = validator.Validate(&logEntry)
+ assert.NoError(t, err)
+ })
+
+ t.Run("with details", func(t *testing.T) {
+ api, _ := NewMockAuditApi()
+ sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator()
+ tracer := otel.Tracer("test")
+
+ objectId := uuid.NewString()
+ operation := "stackit.demo-service.v1.operation"
+ details := map[string]interface{}{"key": "detail"}
+ permission := "project.edit"
+ permissionCheckResult := true
+ requestTime := time.Now().AddDate(0, 0, -2).UTC()
+ responseTime := time.Now().AddDate(0, 0, -1).UTC()
+ responseBody := map[string]interface{}{"key": "response"}
+ responseBodyBytes, err := ResponseBodyToBytes(responseBody)
+ assert.NoError(t, err)
+ builder := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", "worker-id", "eu01").
+ WithRequiredObjectId(objectId).
+ WithRequiredObjectType(ObjectTypeProject).
+ WithRequiredOperation(operation).
+ WithRequiredApiRequest(ApiRequest{
+ Body: nil,
+ Header: TestHeaders,
+ Host: "localhost",
+ Method: "POST",
+ Scheme: "https",
+ Proto: "HTTP/1.1",
+ URL: RequestUrl{
+ Path: "/",
+ RawQuery: nil,
+ },
+ }).
+ WithRequiredRequestClientIp("127.0.0.1").
+ WithAuditPermission(permission).
+ WithAuditPermissionCheckResult(permissionCheckResult).
+ WithDetails(details).
+ WithEventType(EventTypeAdminActivity).
+ WithLabels(map[string]string{"key": "label"}).
+ WithNumResponseItems(int64(10)).
+ WithRequestCorrelationId("correlationId").
+ WithRequestId("requestId").
+ WithRequestTime(requestTime).
+ WithResponseBodyBytes(responseBodyBytes).
+ WithResponseHeaders(map[string][]string{"key": {"header"}}).
+ WithResponseTime(responseTime).
+ WithSeverity(auditV1.LogSeverity_LOG_SEVERITY_ERROR).
+ WithStatusCode(400).
+ WithVisibility(auditV1.Visibility_VISIBILITY_PRIVATE)
+
+ routableIdentifier := RoutableIdentifier{Identifier: objectId, Type: ObjectTypeProject}
+
+ cloudEvent, routingIdentifier, err := builder.Build(context.Background(), SequenceNumber(1))
+ assert.NoError(t, err)
+ assert.True(t, builder.IsBuilt())
+
+ assert.Equal(t, &routableIdentifier, routingIdentifier)
+
+ assert.NotNil(t, cloudEvent)
+ assert.Equal(t, "application/cloudevents+protobuf", cloudEvent.DataContentType)
+ assert.Equal(t, "audit.v1.RoutableAuditEvent", cloudEvent.DataType)
+ assert.Regexp(t, "[0-9]+/eu01/worker-id/1", cloudEvent.Id)
+ assert.Equal(t, "demo-service", cloudEvent.Source)
+ assert.Equal(t, "1.0", cloudEvent.SpecVersion)
+ assert.Equal(t, fmt.Sprintf("projects/%s", objectId), cloudEvent.Subject)
+ assert.NotNil(t, cloudEvent.Time)
+ assert.Equal(t, "00-00000000000000000000000000000000-0000000000000000-00", *cloudEvent.TraceParent)
+ assert.Nil(t, cloudEvent.TraceState)
+
+ var routableAuditEvent auditV1.RoutableAuditEvent
+ assert.NotNil(t, cloudEvent.Data)
+ assert.NoError(t, proto.Unmarshal(cloudEvent.Data, &routableAuditEvent))
+
+ assert.Equal(t, routableIdentifier.ToObjectIdentifier().Identifier, routableAuditEvent.ObjectIdentifier.Identifier)
+ assert.Equal(t, routableIdentifier.ToObjectIdentifier().Type, routableAuditEvent.ObjectIdentifier.Type)
+ assert.Equal(t, auditV1.Visibility_VISIBILITY_PRIVATE, routableAuditEvent.Visibility)
+ assert.Equal(t, operation, routableAuditEvent.OperationName)
+
+ var logEntry auditV1.AuditLogEntry
+ assert.NotNil(t, routableAuditEvent.GetUnencryptedData().Data)
+ assert.NoError(t, proto.Unmarshal(routableAuditEvent.GetUnencryptedData().Data, &logEntry))
+
+ assert.Equal(t, fmt.Sprintf("projects/%s/logs/admin-activity", objectId), logEntry.LogName)
+ assert.Equal(t, map[string]string{"key": "label"}, logEntry.Labels)
+ assert.Nil(t, logEntry.TraceState)
+ assert.Nil(t, logEntry.TraceParent)
+ assert.Equal(t, auditV1.LogSeverity_LOG_SEVERITY_ERROR, logEntry.Severity)
+ assert.NotNil(t, logEntry.Timestamp)
+ assert.Equal(t, "correlationId", *logEntry.CorrelationId)
+ assert.Regexp(t, "[0-9]+/eu01/worker-id/1", logEntry.InsertId)
+
+ assert.NotNil(t, logEntry.ProtoPayload)
+
+ authenticationInfo := logEntry.ProtoPayload.AuthenticationInfo
+ assert.NotNil(t, authenticationInfo)
+ assert.Equal(t, "Christian.Schaible@novatec-gmbh.de", authenticationInfo.PrincipalEmail)
+ assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", authenticationInfo.PrincipalId)
+ assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
+ assert.Nil(t, authenticationInfo.ServiceAccountName)
+
+ assert.Equal(t, []*auditV1.AuthorizationInfo{{
+ Resource: fmt.Sprintf("projects/%s", objectId),
+ Permission: &permission,
+ Granted: &permissionCheckResult,
+ }}, logEntry.ProtoPayload.AuthorizationInfo)
+
+ expectedMetadata, _ := structpb.NewStruct(details)
+ assert.Equal(t, expectedMetadata, logEntry.ProtoPayload.Metadata)
+ assert.Equal(t, "stackit.demo-service.v1.operation", logEntry.ProtoPayload.OperationName)
+ assert.Nil(t, logEntry.ProtoPayload.Request)
+
+ requestMetadata := logEntry.ProtoPayload.RequestMetadata
+ assert.NotNil(t, requestMetadata)
+ assert.Equal(t, "127.0.0.1", requestMetadata.CallerIp)
+ assert.Equal(t, "custom", requestMetadata.CallerSuppliedUserAgent)
+
+ requestAttributes := requestMetadata.RequestAttributes
+ assert.NotNil(t, requestAttributes)
+ assert.Equal(t, "/", requestAttributes.Path)
+ assert.NotNil(t, requestAttributes.Time)
+ assert.Equal(t, "localhost", requestAttributes.Host)
+ assert.Equal(t, auditV1.AttributeContext_HTTP_METHOD_POST, requestAttributes.Method)
+ assert.Equal(t, "requestId", *requestAttributes.Id)
+ assert.Equal(t, "https", requestAttributes.Scheme)
+ assert.Equal(t, map[string]string{"user-agent": "custom"}, requestAttributes.Headers)
+ assert.Nil(t, requestAttributes.Query)
+ assert.Equal(t, "HTTP/1.1", requestAttributes.Protocol)
+
+ requestAttributesAuth := requestAttributes.Auth
+ assert.NotNil(t, requestAttributesAuth)
+ assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63/https%3A%2F%2Faccounts.dev.stackit.cloud", requestAttributesAuth.Principal)
+ assert.Equal(t, []string{"stackit-portal-login-dev-client-id"}, requestAttributesAuth.Audiences)
+ assert.NotNil(t, requestAttributesAuth.Claims)
+
+ expectedResponse, _ := structpb.NewStruct(responseBody)
+ assert.Equal(t, fmt.Sprintf("projects/%s", objectId), logEntry.ProtoPayload.ResourceName)
+ assert.Equal(t, expectedResponse, logEntry.ProtoPayload.Response)
+
+ responseMetadata := logEntry.ProtoPayload.ResponseMetadata
+ assert.NotNil(t, responseMetadata)
+ assert.Nil(t, responseMetadata.ErrorDetails)
+ assert.Equal(t, "Client error", *responseMetadata.ErrorMessage)
+ assert.Equal(t, wrapperspb.Int32(400), responseMetadata.StatusCode)
+
+ responseAttributes := responseMetadata.ResponseAttributes
+ assert.NotNil(t, responseAttributes)
+ assert.Equal(t, map[string]string{"key": "header"}, responseAttributes.Headers)
+ assert.Equal(t, wrapperspb.Int64(10), responseAttributes.NumResponseItems)
+ assert.Equal(t, wrapperspb.Int64(18), responseAttributes.Size)
+ assert.NotNil(t, responseAttributes.Time)
+
+ assert.Equal(t, "demo-service", logEntry.ProtoPayload.ServiceName)
+
+ validator, err := protovalidate.New()
+ assert.NoError(t, err)
+ err = validator.Validate(&logEntry)
+ assert.NoError(t, err)
+ })
+
+ t.Run("system event with object reference", func(t *testing.T) {
+ api, _ := NewMockAuditApi()
+ sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator()
+ tracer := otel.Tracer("test")
+
+ objectId := uuid.NewString()
+ operation := "stackit.demo-service.v1.operation"
+ builder := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", "worker-id", "eu01").
+ WithRequiredObjectId(objectId).
+ WithRequiredObjectType(ObjectTypeProject).
+ WithRequiredOperation(operation).
+ AsSystemEvent()
+
+ cloudEvent, routingIdentifier, err := builder.Build(context.Background(), SequenceNumber(1))
+ assert.NoError(t, err)
+ assert.True(t, builder.IsBuilt())
+
+ assert.Equal(t, SystemIdentifier.Identifier, routingIdentifier.ToObjectIdentifier().Identifier)
+ assert.Equal(t, SystemIdentifier.Type, routingIdentifier.ToObjectIdentifier().Type)
+
+ assert.NotNil(t, cloudEvent)
+ assert.Equal(t, "application/cloudevents+protobuf", cloudEvent.DataContentType)
+ assert.Equal(t, "audit.v1.RoutableAuditEvent", cloudEvent.DataType)
+ assert.Regexp(t, "[0-9]+/eu01/worker-id/1", cloudEvent.Id)
+ assert.Equal(t, "demo-service", cloudEvent.Source)
+ assert.Equal(t, "1.0", cloudEvent.SpecVersion)
+ assert.Equal(t, fmt.Sprintf("projects/%s", objectId), cloudEvent.Subject)
+ assert.NotNil(t, cloudEvent.Time)
+ assert.Equal(t, "00-00000000000000000000000000000000-0000000000000000-00", *cloudEvent.TraceParent)
+ assert.Nil(t, cloudEvent.TraceState)
+
+ var routableAuditEvent auditV1.RoutableAuditEvent
+ assert.NotNil(t, cloudEvent.Data)
+ assert.NoError(t, proto.Unmarshal(cloudEvent.Data, &routableAuditEvent))
+
+ assert.Equal(t, SystemIdentifier.Identifier, routableAuditEvent.ObjectIdentifier.Identifier)
+ assert.Equal(t, SystemIdentifier.Type, routableAuditEvent.ObjectIdentifier.Type)
+ assert.Equal(t, auditV1.Visibility_VISIBILITY_PRIVATE, routableAuditEvent.Visibility)
+ assert.Equal(t, operation, routableAuditEvent.OperationName)
+
+ var logEntry auditV1.AuditLogEntry
+ assert.NotNil(t, routableAuditEvent.GetUnencryptedData().Data)
+ assert.NoError(t, proto.Unmarshal(routableAuditEvent.GetUnencryptedData().Data, &logEntry))
+
+ assert.Equal(t, fmt.Sprintf("system/%s/logs/system-event", uuid.Nil.String()), logEntry.LogName)
+ assert.Nil(t, logEntry.Labels)
+ assert.Nil(t, logEntry.TraceState)
+ assert.Nil(t, logEntry.TraceParent)
+ assert.Equal(t, auditV1.LogSeverity_LOG_SEVERITY_DEFAULT, logEntry.Severity)
+ assert.NotNil(t, logEntry.Timestamp)
+ assert.Nil(t, logEntry.CorrelationId)
+ assert.Regexp(t, "[0-9]+/eu01/worker-id/1", logEntry.InsertId)
+
+ assert.NotNil(t, logEntry.ProtoPayload)
+
+ authenticationInfo := logEntry.ProtoPayload.AuthenticationInfo
+ assert.NotNil(t, authenticationInfo)
+ assert.Equal(t, "do-not-reply@stackit.cloud", authenticationInfo.PrincipalEmail)
+ assert.Equal(t, "none", authenticationInfo.PrincipalId)
+ assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
+ assert.Nil(t, authenticationInfo.ServiceAccountName)
+
+ assert.Nil(t, logEntry.ProtoPayload.AuthorizationInfo)
+ assert.Nil(t, logEntry.ProtoPayload.Metadata)
+ assert.Equal(t, operation, logEntry.ProtoPayload.OperationName)
+ assert.Nil(t, logEntry.ProtoPayload.Request)
+
+ requestMetadata := logEntry.ProtoPayload.RequestMetadata
+ assert.NotNil(t, requestMetadata)
+ assert.Equal(t, "0.0.0.0", requestMetadata.CallerIp)
+ assert.Equal(t, "none", requestMetadata.CallerSuppliedUserAgent)
+
+ requestAttributes := requestMetadata.RequestAttributes
+ assert.NotNil(t, requestAttributes)
+ assert.Equal(t, "none", requestAttributes.Path)
+ assert.NotNil(t, requestAttributes.Time)
+ assert.Equal(t, "0.0.0.0", requestAttributes.Host)
+ assert.Equal(t, auditV1.AttributeContext_HTTP_METHOD_OTHER, requestAttributes.Method)
+ assert.Nil(t, requestAttributes.Id)
+ assert.Equal(t, "none", requestAttributes.Scheme)
+ assert.Equal(t, map[string]string{"user-agent": "none"}, requestAttributes.Headers)
+ assert.Nil(t, requestAttributes.Query)
+ assert.Equal(t, "none", requestAttributes.Protocol)
+
+ requestAttributesAuth := requestAttributes.Auth
+ assert.NotNil(t, requestAttributesAuth)
+ assert.Equal(t, "none/none", requestAttributesAuth.Principal)
+ assert.Nil(t, requestAttributesAuth.Audiences)
+ assert.NotNil(t, requestAttributesAuth.Claims)
+ assert.Equal(t, map[string]any{}, requestAttributesAuth.Claims.AsMap())
+
+ assert.Equal(t, fmt.Sprintf("projects/%s", objectId), logEntry.ProtoPayload.ResourceName)
+ assert.Nil(t, logEntry.ProtoPayload.Response)
+
+ responseMetadata := logEntry.ProtoPayload.ResponseMetadata
+ assert.NotNil(t, responseMetadata)
+ assert.Nil(t, responseMetadata.ErrorDetails)
+ assert.Nil(t, responseMetadata.ErrorMessage)
+ assert.Equal(t, wrapperspb.Int32(200), responseMetadata.StatusCode)
+
+ responseAttributes := responseMetadata.ResponseAttributes
+ assert.NotNil(t, responseAttributes)
+ assert.Nil(t, responseAttributes.Headers)
+ assert.Nil(t, responseAttributes.NumResponseItems)
+ assert.Nil(t, responseAttributes.Size)
+ assert.NotNil(t, responseAttributes.Time)
+
+ assert.Equal(t, "demo-service", logEntry.ProtoPayload.ServiceName)
+
+ validator, err := protovalidate.New()
+ assert.NoError(t, err)
+ err = validator.Validate(&logEntry)
+ assert.NoError(t, err)
+ })
+
+ t.Run("system event", func(t *testing.T) {
+ api, _ := NewMockAuditApi()
+ sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator()
+ tracer := otel.Tracer("test")
+
+ operation := "stackit.demo-service.v1.operation"
+ builder := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", "worker-id", "eu01").
+ WithRequiredOperation(operation).
+ AsSystemEvent()
+
+ cloudEvent, routingIdentifier, err := builder.Build(context.Background(), SequenceNumber(1))
+ assert.NoError(t, err)
+ assert.True(t, builder.IsBuilt())
+
+ assert.Equal(t, SystemIdentifier.Identifier, routingIdentifier.ToObjectIdentifier().Identifier)
+ assert.Equal(t, SystemIdentifier.Type, routingIdentifier.ToObjectIdentifier().Type)
+
+ assert.NotNil(t, cloudEvent)
+ assert.Equal(t, "application/cloudevents+protobuf", cloudEvent.DataContentType)
+ assert.Equal(t, "audit.v1.RoutableAuditEvent", cloudEvent.DataType)
+ assert.Regexp(t, "[0-9]+/eu01/worker-id/1", cloudEvent.Id)
+ assert.Equal(t, "demo-service", cloudEvent.Source)
+ assert.Equal(t, "1.0", cloudEvent.SpecVersion)
+ assert.Equal(t, fmt.Sprintf("system/%s", uuid.Nil.String()), cloudEvent.Subject)
+ assert.NotNil(t, cloudEvent.Time)
+ assert.Equal(t, "00-00000000000000000000000000000000-0000000000000000-00", *cloudEvent.TraceParent)
+ assert.Nil(t, cloudEvent.TraceState)
+
+ var routableAuditEvent auditV1.RoutableAuditEvent
+ assert.NotNil(t, cloudEvent.Data)
+ assert.NoError(t, proto.Unmarshal(cloudEvent.Data, &routableAuditEvent))
+
+ assert.Equal(t, SystemIdentifier.Identifier, routableAuditEvent.ObjectIdentifier.Identifier)
+ assert.Equal(t, SystemIdentifier.Type, routableAuditEvent.ObjectIdentifier.Type)
+ assert.Equal(t, auditV1.Visibility_VISIBILITY_PRIVATE, routableAuditEvent.Visibility)
+ assert.Equal(t, operation, routableAuditEvent.OperationName)
+
+ var logEntry auditV1.AuditLogEntry
+ assert.NotNil(t, routableAuditEvent.GetUnencryptedData().Data)
+ assert.NoError(t, proto.Unmarshal(routableAuditEvent.GetUnencryptedData().Data, &logEntry))
+
+ assert.Equal(t, fmt.Sprintf("system/%s/logs/system-event", uuid.Nil.String()), logEntry.LogName)
+ assert.Nil(t, logEntry.Labels)
+ assert.Nil(t, logEntry.TraceState)
+ assert.Nil(t, logEntry.TraceParent)
+ assert.Equal(t, auditV1.LogSeverity_LOG_SEVERITY_DEFAULT, logEntry.Severity)
+ assert.NotNil(t, logEntry.Timestamp)
+ assert.Nil(t, logEntry.CorrelationId)
+ assert.Regexp(t, "[0-9]+/eu01/worker-id/1", logEntry.InsertId)
+
+ assert.NotNil(t, logEntry.ProtoPayload)
+
+ authenticationInfo := logEntry.ProtoPayload.AuthenticationInfo
+ assert.NotNil(t, authenticationInfo)
+ assert.Equal(t, "do-not-reply@stackit.cloud", authenticationInfo.PrincipalEmail)
+ assert.Equal(t, "none", authenticationInfo.PrincipalId)
+ assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
+ assert.Nil(t, authenticationInfo.ServiceAccountName)
+
+ assert.Nil(t, logEntry.ProtoPayload.AuthorizationInfo)
+ assert.Nil(t, logEntry.ProtoPayload.Metadata)
+ assert.Equal(t, operation, logEntry.ProtoPayload.OperationName)
+ assert.Nil(t, logEntry.ProtoPayload.Request)
+
+ requestMetadata := logEntry.ProtoPayload.RequestMetadata
+ assert.NotNil(t, requestMetadata)
+ assert.Equal(t, "0.0.0.0", requestMetadata.CallerIp)
+ assert.Equal(t, "none", requestMetadata.CallerSuppliedUserAgent)
+
+ requestAttributes := requestMetadata.RequestAttributes
+ assert.NotNil(t, requestAttributes)
+ assert.Equal(t, "none", requestAttributes.Path)
+ assert.NotNil(t, requestAttributes.Time)
+ assert.Equal(t, "0.0.0.0", requestAttributes.Host)
+ assert.Equal(t, auditV1.AttributeContext_HTTP_METHOD_OTHER, requestAttributes.Method)
+ assert.Nil(t, requestAttributes.Id)
+ assert.Equal(t, "none", requestAttributes.Scheme)
+ assert.Equal(t, map[string]string{"user-agent": "none"}, requestAttributes.Headers)
+ assert.Nil(t, requestAttributes.Query)
+ assert.Equal(t, "none", requestAttributes.Protocol)
+
+ requestAttributesAuth := requestAttributes.Auth
+ assert.NotNil(t, requestAttributesAuth)
+ assert.Equal(t, "none/none", requestAttributesAuth.Principal)
+ assert.Nil(t, requestAttributesAuth.Audiences)
+ assert.NotNil(t, requestAttributesAuth.Claims)
+ assert.Equal(t, map[string]any{}, requestAttributesAuth.Claims.AsMap())
+
+ assert.Equal(t, fmt.Sprintf("system/%s", uuid.Nil.String()), logEntry.ProtoPayload.ResourceName)
+ assert.Nil(t, logEntry.ProtoPayload.Response)
+
+ responseMetadata := logEntry.ProtoPayload.ResponseMetadata
+ assert.NotNil(t, responseMetadata)
+ assert.Nil(t, responseMetadata.ErrorDetails)
+ assert.Nil(t, responseMetadata.ErrorMessage)
+ assert.Equal(t, wrapperspb.Int32(200), responseMetadata.StatusCode)
+
+ responseAttributes := responseMetadata.ResponseAttributes
+ assert.NotNil(t, responseAttributes)
+ assert.Nil(t, responseAttributes.Headers)
+ assert.Nil(t, responseAttributes.NumResponseItems)
+ assert.Nil(t, responseAttributes.Size)
+ assert.NotNil(t, responseAttributes.Time)
+
+ assert.Equal(t, "demo-service", logEntry.ProtoPayload.ServiceName)
+
+ validator, err := protovalidate.New()
+ assert.NoError(t, err)
+ err = validator.Validate(&logEntry)
+ assert.NoError(t, err)
+ })
+
+ t.Run("with responsebody unserialized", func(t *testing.T) {
+ api, _ := NewMockAuditApi()
+ sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator()
+ tracer := otel.Tracer("test")
+
+ objectId := uuid.NewString()
+ operation := "stackit.demo-service.v1.operation"
+ details := map[string]interface{}{"key": "detail"}
+ permission := "project.edit"
+ permissionCheckResult := true
+ requestTime := time.Now().AddDate(0, 0, -2).UTC()
+ responseTime := time.Now().AddDate(0, 0, -1).UTC()
+ responseBody := map[string]interface{}{"key": "response"}
+ builder := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", "worker-id", "eu01").
+ WithRequiredObjectId(objectId).
+ WithRequiredObjectType(ObjectTypeProject).
+ WithRequiredOperation(operation).
+ WithRequiredApiRequest(ApiRequest{
+ Body: nil,
+ Header: TestHeaders,
+ Host: "localhost",
+ Method: "POST",
+ Scheme: "https",
+ Proto: "HTTP/1.1",
+ URL: RequestUrl{
+ Path: "/",
+ RawQuery: nil,
+ },
+ }).
+ WithRequiredRequestClientIp("127.0.0.1").
+ WithAuditPermission(permission).
+ WithAuditPermissionCheckResult(permissionCheckResult).
+ WithDetails(details).
+ WithEventType(EventTypeAdminActivity).
+ WithLabels(map[string]string{"key": "label"}).
+ WithNumResponseItems(int64(10)).
+ WithRequestCorrelationId("correlationId").
+ WithRequestId("requestId").
+ WithRequestTime(requestTime).
+ WithResponseBody(responseBody).
+ WithResponseHeaders(map[string][]string{"key": {"header"}}).
+ WithResponseTime(responseTime).
+ WithSeverity(auditV1.LogSeverity_LOG_SEVERITY_ERROR).
+ WithStatusCode(400).
+ WithVisibility(auditV1.Visibility_VISIBILITY_PRIVATE)
+
+ routableIdentifier := RoutableIdentifier{Identifier: objectId, Type: ObjectTypeProject}
+
+ cloudEvent, routingIdentifier, err := builder.Build(context.Background(), SequenceNumber(1))
+ assert.NoError(t, err)
+ assert.True(t, builder.IsBuilt())
+
+ assert.Equal(t, &routableIdentifier, routingIdentifier)
+ assert.NotNil(t, cloudEvent)
+
+ var routableAuditEvent auditV1.RoutableAuditEvent
+ assert.NotNil(t, cloudEvent.Data)
+ assert.NoError(t, proto.Unmarshal(cloudEvent.Data, &routableAuditEvent))
+
+ var logEntry auditV1.AuditLogEntry
+ assert.NotNil(t, routableAuditEvent.GetUnencryptedData().Data)
+ assert.NoError(t, proto.Unmarshal(routableAuditEvent.GetUnencryptedData().Data, &logEntry))
+ assert.NotNil(t, logEntry.ProtoPayload)
+
+ expectedResponse, _ := structpb.NewStruct(responseBody)
+ assert.Equal(t, fmt.Sprintf("projects/%s", objectId), logEntry.ProtoPayload.ResourceName)
+ assert.Equal(t, expectedResponse, logEntry.ProtoPayload.Response)
+
+ validator, err := protovalidate.New()
+ assert.NoError(t, err)
+ err = validator.Validate(&logEntry)
+ assert.NoError(t, err)
+ })
+
+ t.Run("no entry builder", func(t *testing.T) {
+ api, _ := NewMockAuditApi()
+ sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator()
+ tracer := otel.Tracer("test")
+
+ cloudEvent, routingIdentifier, err := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", "worker-id", "eu01").
+ WithAuditLogEntryBuilder(nil).Build(context.Background(), SequenceNumber(1))
+
+ assert.EqualError(t, err, "audit log entry builder not set")
+ assert.Nil(t, cloudEvent)
+ assert.Nil(t, routingIdentifier)
+ })
+
+ t.Run("next sequence number", func(t *testing.T) {
+ api, _ := NewMockAuditApi()
+ sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator()
+ tracer := otel.Tracer("test")
+
+ builder := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", "worker-id", "eu01")
+ assert.Equal(t, SequenceNumber(0), builder.NextSequenceNumber())
+ assert.Equal(t, SequenceNumber(1), builder.NextSequenceNumber())
+ })
+
+ t.Run("revert sequence number", func(t *testing.T) {
+ api, _ := NewMockAuditApi()
+ sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator()
+ tracer := otel.Tracer("test")
+
+ builder := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", "worker-id", "eu01")
+ assert.Equal(t, SequenceNumber(0), builder.NextSequenceNumber())
+ builder.RevertSequenceNumber()
+ assert.Equal(t, SequenceNumber(0), builder.NextSequenceNumber())
+ })
+}
diff --git a/audit/api/converter.go b/audit/api/converter.go
new file mode 100644
index 0000000..c2f5971
--- /dev/null
+++ b/audit/api/converter.go
@@ -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
+ }
+}
diff --git a/audit/api/log.go b/audit/api/log.go
new file mode 100644
index 0000000..ec75b1b
--- /dev/null
+++ b/audit/api/log.go
@@ -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{}
+}
diff --git a/audit/api/log_test.go b/audit/api/log_test.go
new file mode 100644
index 0000000..491a1b7
--- /dev/null
+++ b/audit/api/log_test.go
@@ -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))
+ })
+}
diff --git a/audit/api/model.go b/audit/api/model.go
new file mode 100644
index 0000000..8ad185e
--- /dev/null
+++ b/audit/api/model.go
@@ -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:
+ // 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: ///
+ // 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: //logs/
+ // 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....
+ // 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: /[/locations/][/]
+ // 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 "/" 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: ---
+ // 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
+ }
+}
diff --git a/audit/api/model_test.go b/audit/api/model_test.go
new file mode 100644
index 0000000..073d31a
--- /dev/null
+++ b/audit/api/model_test.go
@@ -0,0 +1,1121 @@
+package api
+
+import (
+ "context"
+ auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
+ "encoding/json"
+ "fmt"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "go.opentelemetry.io/otel"
+ "go.opentelemetry.io/otel/trace"
+ "google.golang.org/protobuf/encoding/protojson"
+ "google.golang.org/protobuf/types/known/structpb"
+ "google.golang.org/protobuf/types/known/timestamppb"
+ "google.golang.org/protobuf/types/known/wrapperspb"
+ "net/http"
+ "net/url"
+ "strings"
+ "testing"
+ "time"
+)
+
+type mockSpan struct {
+ spanContext trace.SpanContext
+}
+
+func (s *mockSpan) SpanContext() trace.SpanContext {
+ return s.spanContext
+}
+
+func Test_TraceParentFromSpan(t *testing.T) {
+ tracer := otel.Tracer("test")
+
+ verifyTraceParent := func(traceParent string, span trace.Span, isSampled bool) {
+ parts := strings.Split(traceParent, "-")
+ assert.Equal(t, 4, len(parts))
+
+ // trace version
+ assert.Equal(t, "00", parts[0])
+ assert.Equal(t, span.SpanContext().TraceID().String(), parts[1])
+ assert.Equal(t, span.SpanContext().SpanID().String(), parts[2])
+
+ var traceFlags = "00"
+ if isSampled {
+ traceFlags = "01"
+ }
+ assert.Equal(t, traceFlags, parts[3])
+ }
+
+ t.Run("sampled", func(t *testing.T) {
+ _, span := tracer.Start(context.Background(), "test")
+ updatedFlags := span.SpanContext().TraceFlags().WithSampled(true)
+ updatedContext := span.SpanContext().WithTraceFlags(updatedFlags)
+
+ mockedSpan := mockSpan{
+ spanContext: updatedContext,
+ }
+
+ traceParent := TraceParentFromSpan(&mockedSpan)
+ verifyTraceParent(traceParent, span, true)
+ })
+
+ t.Run("non-sampled", func(t *testing.T) {
+ _, span := tracer.Start(context.Background(), "test")
+ traceParent := TraceParentFromSpan(span)
+ verifyTraceParent(traceParent, span, false)
+ })
+}
+
+func Test_GetCalledServiceNameFromRequest(t *testing.T) {
+
+ t.Run("request is nil", func(t *testing.T) {
+ serviceName := GetCalledServiceNameFromRequest(nil, "resource-manager")
+ assert.Equal(t, "resource-manager", serviceName)
+ })
+
+ t.Run("localhost", func(t *testing.T) {
+ request := ApiRequest{Host: "localhost:8080"}
+ serviceName := GetCalledServiceNameFromRequest(&request, "resource-manager")
+ assert.Equal(t, "resource-manager", serviceName)
+ })
+
+ t.Run("cf", func(t *testing.T) {
+ request := ApiRequest{Host: "stackit-resource-manager-go-dev.apps.01.cf.eu01.stackit.cloud"}
+ serviceName := GetCalledServiceNameFromRequest(&request, "resource-manager")
+ assert.Equal(t, "stackit-resource-manager-go-dev", serviceName)
+ })
+
+ t.Run("cf invalid host", func(t *testing.T) {
+ request := ApiRequest{Host: ""}
+ serviceName := GetCalledServiceNameFromRequest(&request, "resource-manager")
+ assert.Equal(t, "resource-manager", serviceName)
+ })
+
+ t.Run("ip", func(t *testing.T) {
+ request := ApiRequest{Host: "127.0.0.1"}
+ serviceName := GetCalledServiceNameFromRequest(&request, "resource-manager")
+ assert.Equal(t, "resource-manager", serviceName)
+ },
+ )
+
+ t.Run("ip short", func(t *testing.T) {
+ request := ApiRequest{Host: "::1"}
+ serviceName := GetCalledServiceNameFromRequest(&request, "resource-manager")
+ assert.Equal(t, "resource-manager", serviceName)
+ },
+ )
+}
+
+func Test_NewPbInt64Value(t *testing.T) {
+
+ t.Run("nil", func(t *testing.T) {
+ value := NewPbInt64Value(nil)
+ assert.Nil(t, value)
+ })
+
+ t.Run("value", func(t *testing.T) {
+ var input int64 = 1
+ value := NewPbInt64Value(&input)
+ assert.Equal(t, wrapperspb.Int64Value{Value: 1}.Value, value.Value)
+ })
+}
+
+func Test_NewResponseMetadata(t *testing.T) {
+
+ headers := make(map[string]string)
+ headers["Content-Type"] = "application/json"
+ responseTime := time.Now().UTC()
+ responseItems := int64(10)
+ responseSize := int64(100)
+
+ t.Run("no error", func(t *testing.T) {
+ for code := 1; code < 400; code++ {
+ metadata := NewResponseMetadata(code, &responseItems, &responseSize, headers, responseTime)
+ assert.Equal(t, wrapperspb.Int32(int32(code)).Value, metadata.StatusCode.Value)
+ assert.Nil(t, metadata.ErrorMessage)
+ assert.Nil(t, metadata.ErrorDetails)
+ assert.Equal(t, wrapperspb.Int64(responseItems), metadata.ResponseAttributes.NumResponseItems)
+ assert.Equal(t, wrapperspb.Int64(responseSize), metadata.ResponseAttributes.Size)
+ assert.Equal(t, timestamppb.New(responseTime), metadata.ResponseAttributes.Time)
+ }
+ })
+
+ t.Run("client error", func(t *testing.T) {
+ for code := 400; code < 500; code++ {
+ metadata := NewResponseMetadata(code, nil, nil, headers, responseTime)
+ assert.Equal(t, wrapperspb.Int32(int32(code)).Value, metadata.StatusCode.Value)
+ assert.Equal(t, "Client error", *metadata.ErrorMessage)
+ assert.Nil(t, metadata.ErrorDetails)
+ assert.Nil(t, metadata.ResponseAttributes.NumResponseItems)
+ assert.Nil(t, metadata.ResponseAttributes.Size)
+ assert.Equal(t, timestamppb.New(responseTime), metadata.ResponseAttributes.Time)
+ }
+ })
+
+ t.Run("server error", func(t *testing.T) {
+ for code := 500; code < 600; code++ {
+ metadata := NewResponseMetadata(code, nil, nil, headers, responseTime)
+ assert.Equal(t, wrapperspb.Int32(int32(code)).Value, metadata.StatusCode.Value)
+ assert.Equal(t, "Server error", *metadata.ErrorMessage)
+ assert.Nil(t, metadata.ErrorDetails)
+ assert.Nil(t, metadata.ResponseAttributes.NumResponseItems)
+ assert.Nil(t, metadata.ResponseAttributes.Size)
+ assert.Equal(t, timestamppb.New(responseTime), metadata.ResponseAttributes.Time)
+ }
+ })
+}
+
+func Test_NewRequestMetadata(t *testing.T) {
+
+ userAgent := "userAgent"
+ requestHeaders := make(map[string][]string)
+ requestHeaders["User-Agent"] = []string{userAgent}
+ requestHeaders["Custom"] = []string{"customHeader"}
+
+ queryString := "topic=project"
+ request := ApiRequest{
+ Method: "GET",
+ URL: RequestUrl{Path: "/audit/new", RawQuery: &queryString},
+ Host: "localhost:8080",
+ Proto: "HTTP/1.1",
+ Scheme: "http",
+ Header: requestHeaders,
+ }
+
+ requestId := "requestId"
+ requestScheme := "requestScheme"
+ requestTime := time.Now().UTC()
+
+ audiences := []string{"audience"}
+ authenticationPrincipal := "authenticationPrincipal"
+
+ claimMap := make(map[string]interface{})
+ auditClaims, _ := structpb.NewStruct(claimMap)
+
+ clientIp := "clientIp"
+
+ filteredHeaders := make(map[string]string)
+ filteredHeaders["Custom"] = "customHeader"
+ filteredHeaders["User-Agent"] = userAgent
+
+ verifyRequestMetadata := func(requestMetadata *auditV1.RequestMetadata, requestId *string) {
+ assert.Equal(t, clientIp, requestMetadata.CallerIp)
+ assert.Equal(t, userAgent, requestMetadata.CallerSuppliedUserAgent)
+ assert.NotNil(t, requestMetadata.RequestAttributes)
+
+ attributes := requestMetadata.RequestAttributes
+ assert.Equal(t, requestId, attributes.Id)
+ assert.Equal(t, filteredHeaders, attributes.Headers)
+ assert.Equal(t, request.URL.Path, attributes.Path)
+ assert.Equal(t, request.Host, attributes.Host)
+ assert.Equal(t, requestScheme, attributes.Scheme)
+ assert.Equal(t, timestamppb.New(requestTime), attributes.Time)
+ assert.Equal(t, request.Proto, attributes.Protocol)
+ assert.NotNil(t, attributes.Auth)
+
+ auth := attributes.Auth
+ assert.Equal(t, authenticationPrincipal, auth.Principal)
+ assert.Equal(t, audiences, auth.Audiences)
+ assert.Equal(t, auditClaims, auth.Claims)
+ }
+
+ t.Run("with query parameters", func(t *testing.T) {
+ requestMetadata := NewRequestMetadata(
+ &request,
+ filteredHeaders,
+ &requestId,
+ requestScheme,
+ requestTime,
+ clientIp,
+ authenticationPrincipal,
+ audiences,
+ auditClaims,
+ )
+
+ verifyRequestMetadata(requestMetadata, &requestId)
+ assert.Equal(t, "topic%3Dproject", *requestMetadata.RequestAttributes.Query)
+ })
+
+ t.Run("without query parameters", func(t *testing.T) {
+ request := ApiRequest{
+ Method: "GET",
+ URL: RequestUrl{Path: "/audit/new"},
+ Host: "localhost:8080",
+ Proto: "HTTP/1.1",
+ Header: requestHeaders,
+ }
+
+ requestMetadata := NewRequestMetadata(
+ &request,
+ filteredHeaders,
+ &requestId,
+ requestScheme,
+ requestTime,
+ clientIp,
+ authenticationPrincipal,
+ audiences,
+ auditClaims,
+ )
+
+ verifyRequestMetadata(requestMetadata, &requestId)
+ assert.Nil(t, requestMetadata.RequestAttributes.Query)
+ })
+
+ t.Run("with empty query parameters", func(t *testing.T) {
+ emptyQuery := ""
+ request := ApiRequest{
+ Method: "GET",
+ URL: RequestUrl{Path: "/audit/new", RawQuery: &emptyQuery},
+ Host: "localhost:8080",
+ Proto: "HTTP/1.1",
+ Header: requestHeaders,
+ }
+
+ requestMetadata := NewRequestMetadata(
+ &request,
+ filteredHeaders,
+ &requestId,
+ requestScheme,
+ requestTime,
+ clientIp,
+ authenticationPrincipal,
+ audiences,
+ auditClaims,
+ )
+
+ verifyRequestMetadata(requestMetadata, &requestId)
+ assert.Nil(t, requestMetadata.RequestAttributes.Query)
+ })
+
+ t.Run("without request id", func(t *testing.T) {
+ request := ApiRequest{
+ Method: "GET",
+ URL: RequestUrl{Path: "/audit/new", RawQuery: &queryString},
+ Host: "localhost:8080",
+ Proto: "HTTP/1.1",
+ Header: requestHeaders,
+ }
+
+ requestMetadata := NewRequestMetadata(
+ &request, filteredHeaders,
+ nil,
+ requestScheme,
+ requestTime,
+ clientIp,
+ authenticationPrincipal,
+ audiences,
+ auditClaims,
+ )
+ verifyRequestMetadata(requestMetadata, nil)
+ })
+
+ t.Run("various default http methods", func(t *testing.T) {
+ httpMethods := []string{"GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"}
+ for _, httpMethod := range httpMethods {
+ request := ApiRequest{
+ Method: httpMethod,
+ URL: RequestUrl{Path: "/audit/new", RawQuery: &queryString},
+ Host: "localhost:8080",
+ Proto: "HTTP/1.1",
+ Header: requestHeaders,
+ }
+
+ requestMetadata := NewRequestMetadata(
+ &request, filteredHeaders,
+ &requestId, requestScheme,
+ requestTime, clientIp,
+ authenticationPrincipal,
+ audiences,
+ auditClaims,
+ )
+
+ verifyRequestMetadata(requestMetadata, &requestId)
+ expectedMethod := fmt.Sprintf("HTTP_METHOD_%s", httpMethod)
+ assert.Equal(t, expectedMethod, requestMetadata.RequestAttributes.Method.String())
+ }
+ })
+
+ t.Run("unknown http method", func(t *testing.T) {
+ request := ApiRequest{
+ Method: "",
+ URL: RequestUrl{Path: "/audit/new", RawQuery: &queryString},
+ Host: "localhost:8080",
+ Proto: "HTTP/1.1",
+ Header: requestHeaders,
+ }
+
+ requestMetadata := NewRequestMetadata(
+ &request, filteredHeaders,
+ &requestId, requestScheme,
+ requestTime, clientIp,
+ authenticationPrincipal,
+ audiences,
+ auditClaims,
+ )
+
+ verifyRequestMetadata(requestMetadata, &requestId)
+ assert.Equal(t,
+ auditV1.AttributeContext_HTTP_METHOD_UNSPECIFIED.String(),
+ requestMetadata.RequestAttributes.Method.String())
+
+ })
+}
+
+func Test_FilterAndMergeRequestHeaders(t *testing.T) {
+
+ t.Run("skip headers", func(t *testing.T) {
+ headers := make(map[string][]string)
+ headers["Authorization"] = []string{"ey..."}
+ headers["B3"] = []string{"80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-1-05e3ac9a4f6e3b90"}
+ headers[":authority"] = []string{"localhost:9090"}
+
+ filteredHeaders := FilterAndMergeHeaders(headers)
+ assert.Equal(t, 0, len(filteredHeaders))
+ })
+
+ t.Run("skip headers by prefix", func(t *testing.T) {
+ headers := make(map[string][]string)
+ headers["X-Forwarded-Proto"] = []string{"https"}
+ headers["Stackit-test"] = []string{"test"}
+
+ filteredHeaders := FilterAndMergeHeaders(headers)
+ assert.Equal(t, 0, len(filteredHeaders))
+ })
+
+ t.Run("merge headers", func(t *testing.T) {
+ headers := make(map[string][]string)
+ headers["Custom1"] = []string{"value1", "value2"}
+ headers["Custom2"] = []string{"value3", "value4"}
+
+ filteredHeaders := FilterAndMergeHeaders(headers)
+ assert.Equal(t, 2, len(filteredHeaders))
+ assert.Equal(t, "value1,value2", filteredHeaders["Custom1"])
+ assert.Equal(t, "value3,value4", filteredHeaders["Custom2"])
+ })
+
+ t.Run("skip merge headers mixed", func(t *testing.T) {
+ headers := make(map[string][]string)
+ headers["Custom1"] = []string{"value1", "value2"}
+ headers["Custom2"] = []string{"value3"}
+ headers["STACKIT-MIXED"] = []string{"test"}
+
+ filteredHeaders := FilterAndMergeHeaders(headers)
+ assert.Equal(t, 2, len(filteredHeaders))
+ assert.Equal(t, "value1,value2", filteredHeaders["Custom1"])
+ assert.Equal(t, "value3", filteredHeaders["Custom2"])
+ })
+
+ t.Run("Keep empty and blank header values", func(t *testing.T) {
+ headers := make(map[string][]string)
+ headers["empty"] = []string{""}
+ headers["blank"] = []string{" "}
+ filteredHeaders := FilterAndMergeHeaders(headers)
+ assert.Equal(t, 2, len(filteredHeaders))
+ assert.Equal(t, "", filteredHeaders["empty"])
+ assert.Equal(t, " ", filteredHeaders["blank"])
+ })
+}
+
+func Test_AuditAttributesFromAuthorizationHeader(t *testing.T) {
+
+ t.Run("basic token", func(t *testing.T) {
+ headerValue := "Basic username:password"
+ headers := make(map[string][]string)
+ headers["Authorization"] = []string{headerValue}
+ request := ApiRequest{Header: headers}
+
+ _, _, _, _, err := AuditAttributesFromAuthorizationHeader(&request)
+ assert.ErrorIs(t, err, ErrTokenIsNotBearerToken)
+ })
+
+ t.Run("invalid header value", func(t *testing.T) {
+ headerValue := "a b c"
+ headers := make(map[string][]string)
+ headers["Authorization"] = []string{headerValue}
+ request := ApiRequest{Header: headers}
+
+ _, _, _, _, err := AuditAttributesFromAuthorizationHeader(&request)
+ assert.ErrorIs(t, err, ErrInvalidAuthorizationHeaderValue)
+ })
+
+ t.Run("invalid token too many parts", func(t *testing.T) {
+ headerValue := "Bearer a.b.c.d"
+ headers := make(map[string][]string)
+ headers["Authorization"] = []string{headerValue}
+ request := ApiRequest{Header: headers}
+
+ _, _, _, _, err := AuditAttributesFromAuthorizationHeader(&request)
+ assert.ErrorIs(t, err, ErrInvalidBearerToken)
+ })
+
+ t.Run("invalid bearer token", func(t *testing.T) {
+ headerValue := "Bearer a.b.c"
+ headers := make(map[string][]string)
+ headers["Authorization"] = []string{headerValue}
+ request := ApiRequest{Header: headers}
+
+ _, _, _, _, err := AuditAttributesFromAuthorizationHeader(&request)
+ assert.ErrorIs(t, err, ErrInvalidBearerToken)
+ })
+
+ t.Run("client credentials token", func(t *testing.T) {
+ headers := make(map[string][]string)
+ headers["Authorization"] = []string{clientCredentialsToken}
+ request := ApiRequest{Header: headers}
+
+ auditClaims, authenticationPrincipal, audiences, authenticationInfo, err :=
+ AuditAttributesFromAuthorizationHeader(&request)
+
+ auditClaimsMap := auditClaims.AsMap()
+ assert.Nil(t, err)
+ assert.Equal(t, "https://accounts.dev.stackit.cloud", auditClaimsMap["iss"])
+ assert.Equal(t, "stackit-resource-manager-dev", auditClaimsMap["sub"])
+ assert.Equal(t, []interface{}{"stackit-resource-manager-dev"}, auditClaimsMap["aud"].([]interface{}))
+ assert.Equal(t, "e46eba38-dedb-4541-94f3-49f97a934d58", auditClaimsMap["jti"])
+
+ principal := fmt.Sprintf("%s/%s",
+ url.QueryEscape("stackit-resource-manager-dev"),
+ url.QueryEscape("https://accounts.dev.stackit.cloud"))
+ assert.Equal(t, principal, authenticationPrincipal)
+
+ assert.Equal(t, []string{"stackit-resource-manager-dev"}, audiences)
+
+ assert.Equal(t, "stackit-resource-manager-dev", authenticationInfo.PrincipalId)
+ assert.Equal(t, "do-not-reply@stackit.cloud", authenticationInfo.PrincipalEmail)
+
+ assert.Nil(t, authenticationInfo.ServiceAccountName)
+ assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
+ })
+
+ t.Run("service account access token", func(t *testing.T) {
+ headers := make(map[string][]string)
+ headers["Authorization"] = []string{serviceAccountToken}
+ request := ApiRequest{Header: headers}
+
+ auditClaims, authenticationPrincipal, audiences, authenticationInfo, err :=
+ AuditAttributesFromAuthorizationHeader(&request)
+
+ auditClaimsMap := auditClaims.AsMap()
+ assert.Nil(t, err)
+ assert.Equal(t, "stackit/serviceaccount", auditClaimsMap["iss"])
+ assert.Equal(t, "10f38b01-534b-47bb-a03a-e294ca2be4de", auditClaimsMap["sub"])
+ assert.Equal(t, []interface{}{"stackit", "api"}, auditClaimsMap["aud"].([]interface{}))
+ assert.Equal(t, "84c30a46-1001-436f-859f-89c0ba19be1e", auditClaimsMap["jti"])
+
+ principal := fmt.Sprintf("%s/%s",
+ url.QueryEscape("10f38b01-534b-47bb-a03a-e294ca2be4de"),
+ url.QueryEscape("stackit/serviceaccount"))
+ assert.Equal(t, principal, authenticationPrincipal)
+
+ assert.Equal(t, []string{"stackit", "api"}, audiences)
+
+ assert.Equal(t, "10f38b01-534b-47bb-a03a-e294ca2be4de", authenticationInfo.PrincipalId)
+ assert.Equal(t, "my-service-yifc9e1@sa.stackit.cloud", authenticationInfo.PrincipalEmail)
+
+ assert.Equal(t,
+ "projects/dacc7830-843e-4c5e-86ff-aa0fb51d636f/service-accounts/10f38b01-534b-47bb-a03a-e294ca2be4de",
+ *authenticationInfo.ServiceAccountName)
+ assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
+ })
+
+ t.Run("impersonated token of access token", func(t *testing.T) {
+ headers := make(map[string][]string)
+ headers["Authorization"] = []string{serviceAccountTokenImpersonated}
+ request := ApiRequest{Header: headers}
+
+ auditClaims, authenticationPrincipal, audiences, authenticationInfo, err :=
+ AuditAttributesFromAuthorizationHeader(&request)
+
+ auditClaimsMap := auditClaims.AsMap()
+ assert.Nil(t, err)
+ assert.Equal(t, "stackit/serviceaccount", auditClaimsMap["iss"])
+ assert.Equal(t, "f45009b2-6433-43c1-b6c7-618c44359e71", auditClaimsMap["sub"])
+ assert.Equal(t, []interface{}{"stackit", "api"}, auditClaimsMap["aud"].([]interface{}))
+ assert.Equal(t, "37555183-01b9-4270-bdc1-69b4fcfd5ee9", auditClaimsMap["jti"])
+
+ principal := fmt.Sprintf("%s/%s",
+ url.QueryEscape("f45009b2-6433-43c1-b6c7-618c44359e71"),
+ url.QueryEscape("stackit/serviceaccount"))
+ assert.Equal(t, principal, authenticationPrincipal)
+
+ assert.Equal(t, []string{"stackit", "api"}, audiences)
+
+ assert.Equal(t, "f45009b2-6433-43c1-b6c7-618c44359e71", authenticationInfo.PrincipalId)
+ assert.Equal(t, "service-account-2-tj9srt1@sa.stackit.cloud", authenticationInfo.PrincipalEmail)
+
+ assert.Equal(t,
+ "projects/dacc7830-843e-4c5e-86ff-aa0fb51d636f/service-accounts/f45009b2-6433-43c1-b6c7-618c44359e71",
+ *authenticationInfo.ServiceAccountName)
+ assert.NotNil(t, authenticationInfo.ServiceAccountDelegationInfo)
+
+ serviceAccountDelegationInfo := authenticationInfo.ServiceAccountDelegationInfo
+ assert.Equal(t, "02aef516-317f-4ec1-a1df-1acbd4d49fe3", serviceAccountDelegationInfo[0].GetIdpPrincipal().PrincipalId)
+ assert.Equal(t, "do-not-reply@stackit.cloud", serviceAccountDelegationInfo[0].GetIdpPrincipal().PrincipalEmail)
+ })
+
+ t.Run("impersonated token of impersonated access token", func(t *testing.T) {
+ headers := make(map[string][]string)
+ headers["Authorization"] = []string{serviceAccountTokenRepeatedlyImpersonated}
+ request := ApiRequest{Header: headers}
+
+ auditClaims, authenticationPrincipal, audiences, authenticationInfo, err :=
+ AuditAttributesFromAuthorizationHeader(&request)
+
+ auditClaimsMap := auditClaims.AsMap()
+ assert.Nil(t, err)
+ assert.Equal(t, "stackit/serviceaccount", auditClaimsMap["iss"])
+ assert.Equal(t, "1734b4b6-1d5e-4819-9b50-29917a1b9ad5", auditClaimsMap["sub"])
+ assert.Equal(t, []interface{}{"stackit", "api"}, auditClaimsMap["aud"].([]interface{}))
+ assert.Equal(t, "1f7f1efc-3349-411a-a5d7-2255e0a5a8ae", auditClaimsMap["jti"])
+
+ principal := fmt.Sprintf("%s/%s",
+ url.QueryEscape("1734b4b6-1d5e-4819-9b50-29917a1b9ad5"),
+ url.QueryEscape("stackit/serviceaccount"))
+ assert.Equal(t, principal, authenticationPrincipal)
+
+ assert.Equal(t, []string{"stackit", "api"}, audiences)
+
+ assert.Equal(t, "1734b4b6-1d5e-4819-9b50-29917a1b9ad5", authenticationInfo.PrincipalId)
+ assert.Equal(t, "service-account-3-fghsxw1@sa.stackit.cloud", authenticationInfo.PrincipalEmail)
+
+ assert.Equal(t,
+ "projects/dacc7830-843e-4c5e-86ff-aa0fb51d636f/service-accounts/1734b4b6-1d5e-4819-9b50-29917a1b9ad5",
+ *authenticationInfo.ServiceAccountName)
+ assert.NotNil(t, authenticationInfo.ServiceAccountDelegationInfo)
+
+ serviceAccountDelegationInfo := authenticationInfo.ServiceAccountDelegationInfo
+ assert.Equal(t, "f45009b2-6433-43c1-b6c7-618c44359e71", serviceAccountDelegationInfo[0].GetIdpPrincipal().PrincipalId)
+ assert.Equal(t, "do-not-reply@stackit.cloud", serviceAccountDelegationInfo[0].GetIdpPrincipal().PrincipalEmail)
+ assert.Equal(t, "02aef516-317f-4ec1-a1df-1acbd4d49fe3", serviceAccountDelegationInfo[1].GetIdpPrincipal().PrincipalId)
+ assert.Equal(t, "do-not-reply@stackit.cloud", serviceAccountDelegationInfo[1].GetIdpPrincipal().PrincipalEmail)
+ })
+
+ t.Run("user token", func(t *testing.T) {
+ headers := make(map[string][]string)
+ headers["Authorization"] = []string{userToken}
+ request := ApiRequest{Header: headers}
+
+ auditClaims, authenticationPrincipal, audiences, authenticationInfo, err :=
+ AuditAttributesFromAuthorizationHeader(&request)
+
+ auditClaimsMap := auditClaims.AsMap()
+ assert.Nil(t, err)
+ assert.Equal(t, "https://accounts.dev.stackit.cloud", auditClaimsMap["iss"])
+ assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", auditClaimsMap["sub"])
+ assert.Equal(t, []interface{}{"stackit-portal-login-dev-client-id"}, auditClaimsMap["aud"].([]interface{}))
+ assert.Equal(t, "d73a67ac-d1ec-4b55-99d4-e953275f022a", auditClaimsMap["jti"])
+
+ principal := fmt.Sprintf("%s/%s",
+ url.QueryEscape("cd94f01a-df2e-4456-902e-48f5e57f0b63"),
+ url.QueryEscape("https://accounts.dev.stackit.cloud"))
+ assert.Equal(t, principal, authenticationPrincipal)
+
+ assert.Equal(t, []string{"stackit-portal-login-dev-client-id"}, audiences)
+
+ assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", authenticationInfo.PrincipalId)
+ assert.Equal(t, "Christian.Schaible@novatec-gmbh.de", authenticationInfo.PrincipalEmail)
+
+ assert.Nil(t, authenticationInfo.ServiceAccountName)
+ assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
+ })
+}
+
+func Test_NewAuditLogEntry(t *testing.T) {
+
+ t.Run("minimum attributes set", func(t *testing.T) {
+ userAgent := "userAgent"
+ requestHeaders := make(map[string][]string)
+ requestHeaders["Authorization"] = []string{userToken}
+ requestHeaders["User-Agent"] = []string{userAgent}
+ requestHeaders["Custom"] = []string{"customHeader"}
+
+ request := ApiRequest{
+ Method: "GET",
+ URL: RequestUrl{Path: "/audit/new"},
+ Host: "localhost:8080",
+ Proto: "HTTP/1.1",
+ Scheme: "http",
+ Header: requestHeaders,
+ }
+
+ clientIp := "127.0.0.1"
+ correlationId := uuid.NewString()
+ auditRequest := AuditRequest{
+ Request: &request,
+ RequestClientIP: clientIp,
+ RequestCorrelationId: &correlationId,
+ RequestId: nil,
+ RequestTime: nil,
+ }
+
+ statusCode := 200
+ auditResponse := AuditResponse{
+ ResponseBodyBytes: nil,
+ ResponseStatusCode: statusCode,
+ ResponseHeaders: nil,
+ ResponseNumItems: nil,
+ ResponseTime: nil,
+ }
+
+ objectId := uuid.NewString()
+ logName := fmt.Sprintf("projects/%s/logs/%s", objectId, EventTypeAdminActivity)
+ serviceName := "resource-manager"
+ operationName := fmt.Sprintf("stackit.%s.v2.projects.updated", serviceName)
+ resourceName := fmt.Sprintf("projects/%s", objectId)
+ auditTime := time.Now().UTC()
+ insertId := fmt.Sprintf("%d/%s/%s/%d", auditTime.UnixNano(), "eu01", "1", 1)
+
+ severity := auditV1.LogSeverity_LOG_SEVERITY_DEFAULT
+ auditMetadata := AuditMetadata{
+ AuditInsertId: insertId,
+ AuditLabels: nil,
+ AuditLogName: logName,
+ AuditLogSeverity: severity,
+ AuditOperationName: operationName,
+ AuditPermission: nil,
+ AuditPermissionGranted: nil,
+ AuditResourceName: resourceName,
+ AuditServiceName: serviceName,
+ AuditTime: nil,
+ }
+
+ logEntry, _ := NewAuditLogEntry(
+ auditRequest,
+ auditResponse,
+ nil,
+ auditMetadata,
+ nil,
+ nil)
+
+ assert.Equal(t, logName, logEntry.LogName)
+ assert.Equal(t, insertId, logEntry.InsertId)
+ assert.Equal(t, &correlationId, logEntry.CorrelationId)
+ assert.Equal(t, severity, logEntry.Severity)
+ assert.NoError(t, logEntry.Timestamp.CheckValid())
+ assert.Nil(t, logEntry.Labels)
+ assert.Nil(t, logEntry.TraceParent)
+ assert.Nil(t, logEntry.TraceState)
+
+ payload := logEntry.ProtoPayload
+ assert.NotNil(t, payload)
+ assert.Equal(t, serviceName, payload.ServiceName)
+ assert.Equal(t, operationName, payload.OperationName)
+ assert.Equal(t, resourceName, payload.ResourceName)
+ assert.Equal(t, int32(statusCode), payload.ResponseMetadata.StatusCode.Value)
+ assert.Nil(t, payload.ResponseMetadata.ErrorMessage)
+ assert.Nil(t, payload.ResponseMetadata.ErrorDetails)
+ assert.Nil(t, payload.ResponseMetadata.ResponseAttributes.Size)
+ assert.NotNil(t, payload.ResponseMetadata.ResponseAttributes.Time)
+ assert.Nil(t, payload.ResponseMetadata.ResponseAttributes.NumResponseItems)
+ assert.Nil(t, payload.ResponseMetadata.ResponseAttributes.Headers)
+
+ assert.Nil(t, payload.Request)
+ assert.Nil(t, payload.Response)
+ assert.Nil(t, payload.Metadata)
+
+ authenticationInfo := payload.AuthenticationInfo
+ assert.NotNil(t, authenticationInfo)
+ assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", authenticationInfo.PrincipalId)
+ assert.Equal(t, "Christian.Schaible@novatec-gmbh.de", authenticationInfo.PrincipalEmail)
+ assert.Nil(t, authenticationInfo.ServiceAccountName)
+ assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
+
+ assert.Nil(t, payload.AuthorizationInfo)
+
+ requestMetadata := payload.RequestMetadata
+ assert.NotNil(t, requestMetadata)
+ assert.Equal(t, clientIp, requestMetadata.CallerIp)
+ assert.Equal(t, userAgent, requestMetadata.CallerSuppliedUserAgent)
+
+ // Don't verify explicitly - trust on other unit test
+ assert.NotNil(t, userAgent, requestMetadata.RequestAttributes)
+ })
+
+ t.Run("all attributes set", func(t *testing.T) {
+ userAgent := "userAgent"
+ requestHeaders := make(map[string][]string)
+ requestHeaders["Authorization"] = []string{userToken}
+ requestHeaders["User-Agent"] = []string{userAgent}
+ requestHeaders["Custom"] = []string{"customHeader"}
+
+ requestBody := make(map[string]interface{})
+ requestBody["key"] = "request"
+ requestBodyBytes, _ := json.Marshal(requestBody)
+ query := "topic=project"
+ request := ApiRequest{
+ Method: "GET",
+ URL: RequestUrl{Path: "/audit/new", RawQuery: &query},
+ Host: "localhost:8080",
+ Proto: "HTTP/1.1",
+ Scheme: "http",
+ Header: requestHeaders,
+ Body: &requestBodyBytes,
+ }
+
+ clientIp := "127.0.0.1"
+ correlationId := uuid.NewString()
+ requestId := uuid.NewString()
+ requestTime := time.Now().UTC()
+ auditRequest := AuditRequest{
+ Request: &request,
+ RequestClientIP: clientIp,
+ RequestCorrelationId: &correlationId,
+ RequestId: &requestId,
+ RequestTime: &requestTime,
+ }
+
+ response := make(map[string]interface{})
+ response["key"] = "value"
+ responseBody, _ := json.Marshal(response)
+ responseHeader := http.Header{}
+ responseHeader.Set("Content-Type", "application/json")
+ responseHeaderMap := make(map[string]string)
+ responseHeaderMap["Content-Type"] = "application/json"
+ responseNumItems := int64(1)
+ responseStatusCode := 400
+ responseTime := time.Now().UTC()
+
+ auditResponse := AuditResponse{
+ ResponseBodyBytes: &responseBody,
+ ResponseStatusCode: responseStatusCode,
+ ResponseHeaders: responseHeader,
+ ResponseNumItems: &responseNumItems,
+ ResponseTime: &responseTime,
+ }
+
+ auditTime := time.Now().UTC()
+
+ objectId := uuid.NewString()
+ logName := fmt.Sprintf("projects/%s/logs/%s", objectId, EventTypeAdminActivity)
+ serviceName := "resource-manager"
+ operationName := fmt.Sprintf("stackit.%s.v2.projects.updated", serviceName)
+ resourceName := fmt.Sprintf("projects/%s", objectId)
+ insertId := fmt.Sprintf("%d/%s/%s/%d", auditTime.UnixNano(), "eu01", "1", 1)
+ permission := "resource-manager.project.edit"
+ permissionGranted := true
+ labels := make(map[string]string)
+ labels["label"] = "value"
+
+ severity := auditV1.LogSeverity_LOG_SEVERITY_DEFAULT
+
+ auditMetadata := AuditMetadata{
+ AuditInsertId: insertId,
+ AuditLabels: &labels,
+ AuditLogName: logName,
+ AuditLogSeverity: severity,
+ AuditOperationName: operationName,
+ AuditPermission: &permission,
+ AuditPermissionGranted: &permissionGranted,
+ AuditResourceName: resourceName,
+ AuditServiceName: serviceName,
+ AuditTime: &auditTime,
+ }
+
+ eventMetadata := map[string]interface{}{"key": "value"}
+
+ traceParent := "traceParent"
+ traceState := "traceState"
+ logEntry, _ := NewAuditLogEntry(
+ auditRequest,
+ auditResponse,
+ &eventMetadata,
+ auditMetadata,
+ &traceParent,
+ &traceState)
+
+ assert.Equal(t, logName, logEntry.LogName)
+ assert.Equal(t, insertId, logEntry.InsertId)
+ assert.Equal(t, labels, logEntry.Labels)
+ assert.Equal(t, correlationId, *logEntry.CorrelationId)
+ assert.Equal(t, timestamppb.New(auditTime), logEntry.Timestamp)
+ assert.Equal(t, severity, logEntry.Severity)
+ assert.Equal(t, &traceParent, logEntry.TraceParent)
+ assert.Equal(t, &traceState, logEntry.TraceState)
+ assert.NotNil(t, logEntry.ProtoPayload)
+
+ payload := logEntry.ProtoPayload
+ assert.NotNil(t, payload)
+ assert.Equal(t, serviceName, payload.ServiceName)
+ assert.Equal(t, operationName, payload.OperationName)
+ assert.Equal(t, resourceName, payload.ResourceName)
+ assert.Equal(t, int32(responseStatusCode), payload.ResponseMetadata.StatusCode.Value)
+ assert.Equal(t, "Client error", *payload.ResponseMetadata.ErrorMessage)
+ assert.Nil(t, payload.ResponseMetadata.ErrorDetails)
+ assert.Equal(t, wrapperspb.Int64(int64(len(responseBody))), payload.ResponseMetadata.ResponseAttributes.Size)
+ assert.Equal(t, timestamppb.New(responseTime), payload.ResponseMetadata.ResponseAttributes.Time)
+ assert.Equal(t, wrapperspb.Int64(responseNumItems), payload.ResponseMetadata.ResponseAttributes.NumResponseItems)
+ assert.Equal(t, responseHeaderMap, payload.ResponseMetadata.ResponseAttributes.Headers)
+
+ expectedRequestBody, _ := structpb.NewStruct(requestBody)
+ assert.Equal(t, expectedRequestBody, payload.Request)
+ expectedResponseBody, _ := structpb.NewStruct(response)
+ assert.Equal(t, expectedResponseBody, payload.Response)
+ expectedEventMetadata, _ := structpb.NewStruct(eventMetadata)
+ assert.Equal(t, expectedEventMetadata, payload.Metadata)
+
+ authenticationInfo := payload.AuthenticationInfo
+ assert.NotNil(t, authenticationInfo)
+ assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", authenticationInfo.PrincipalId)
+ assert.Equal(t, "Christian.Schaible@novatec-gmbh.de", authenticationInfo.PrincipalEmail)
+ assert.Nil(t, authenticationInfo.ServiceAccountName)
+ assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
+
+ authorizationInfo := payload.AuthorizationInfo
+ assert.NotNil(t, authorizationInfo)
+ assert.Equal(t, 1, len(authorizationInfo))
+ assert.Equal(t, permission, *authorizationInfo[0].Permission)
+ assert.Equal(t, permissionGranted, *authorizationInfo[0].Granted)
+ assert.Equal(t, resourceName, authorizationInfo[0].Resource)
+
+ requestMetadata := payload.RequestMetadata
+ assert.NotNil(t, requestMetadata)
+ assert.Equal(t, clientIp, requestMetadata.CallerIp)
+ assert.Equal(t, userAgent, requestMetadata.CallerSuppliedUserAgent)
+
+ // Don't verify explicitly - trust on other unit test
+ assert.NotNil(t, userAgent, requestMetadata.RequestAttributes)
+ })
+}
+
+func Test_NewInsertId(t *testing.T) {
+ insertTime := time.Now().UTC()
+ location := "eu01"
+ workerId := uuid.NewString()
+ var eventSequenceNumber uint64 = 1
+
+ insertId := NewInsertId(insertTime, location, workerId, eventSequenceNumber)
+ expectedId := fmt.Sprintf("%d/%s/%s/%d", insertTime.UnixNano(), location, workerId, eventSequenceNumber)
+ assert.Equal(t, expectedId, insertId)
+}
+
+func Test_NewNewAuditRoutingIdentifier(t *testing.T) {
+ objectId := uuid.NewString()
+ objectType := ObjectTypeProject
+
+ routingIdentifier := NewAuditRoutingIdentifier(objectId, objectType)
+ assert.Equal(t, objectId, routingIdentifier.Identifier)
+ assert.Equal(t, objectType, routingIdentifier.Type)
+}
+
+func Test_OperationNameFromUrlPath(t *testing.T) {
+
+ t.Run("empty path", func(t *testing.T) {
+ operationName := OperationNameFromUrlPath("", "GET")
+ assert.Equal(t, "", operationName)
+ })
+
+ t.Run("root path", func(t *testing.T) {
+ operationName := OperationNameFromUrlPath("/", "GET")
+ assert.Equal(t, "", operationName)
+ })
+
+ t.Run("path without version", func(t *testing.T) {
+ operationName := OperationNameFromUrlPath("/projects", "GET")
+ assert.Equal(t, "projects.read", operationName)
+ })
+
+ t.Run("path with uuid without version", func(t *testing.T) {
+ operationName := OperationNameFromUrlPath("/projects/ac51bbd2-cb23-441b-a2ee-5393189695aa", "GET")
+ assert.Equal(t, "projects.read", operationName)
+ })
+
+ t.Run("path with uuid and version", func(t *testing.T) {
+ operationName := OperationNameFromUrlPath("/v2/projects/ac51bbd2-cb23-441b-a2ee-5393189695aa", "GET")
+ assert.Equal(t, "v2.projects.read", operationName)
+ })
+
+ t.Run("concatenated path", func(t *testing.T) {
+ operationName := OperationNameFromUrlPath("/v2/organizations/ac51bbd2-cb23-441b-a2ee-5393189695aa/folders/167fc176-9d8e-477b-a56c-b50d7b26adcf/projects/0a2a4f9b-4e67-4562-ad02-c2d200e05aa6/audit/policy", "GET")
+ assert.Equal(t, "v2.organizations.folders.projects.audit.policy.read", operationName)
+ })
+
+ t.Run("path with query params", func(t *testing.T) {
+ operationName := OperationNameFromUrlPath("/v2/organizations/ac51bbd2-cb23-441b-a2ee-5393189695aa/audit/policy?since=2024-08-27", "GET")
+ assert.Equal(t, "v2.organizations.audit.policy.read", operationName)
+ })
+
+ t.Run("path trailing slash", func(t *testing.T) {
+ operationName := OperationNameFromUrlPath("/projects/ac51bbd2-cb23-441b-a2ee-5393189695aa/", "GET")
+ assert.Equal(t, "projects.read", operationName)
+ })
+
+ t.Run("path trailing slash and query params", func(t *testing.T) {
+ operationName := OperationNameFromUrlPath("/projects/ac51bbd2-cb23-441b-a2ee-5393189695aa/?changeDate=2024-10-13", "GET")
+ assert.Equal(t, "projects.read", operationName)
+ })
+
+ t.Run("http method post", func(t *testing.T) {
+ operationName := OperationNameFromUrlPath("/projects", "POST")
+ assert.Equal(t, "projects.create", operationName)
+ })
+
+ t.Run("http method put", func(t *testing.T) {
+ operationName := OperationNameFromUrlPath("/projects", "PUT")
+ assert.Equal(t, "projects.update", operationName)
+ })
+
+ t.Run("http method patch", func(t *testing.T) {
+ operationName := OperationNameFromUrlPath("/projects", "PATCH")
+ assert.Equal(t, "projects.update", operationName)
+ })
+
+ t.Run("http method delete", func(t *testing.T) {
+ operationName := OperationNameFromUrlPath("/projects", "DELETE")
+ assert.Equal(t, "projects.delete", operationName)
+ })
+
+ t.Run("operation name fallback on options", func(t *testing.T) {
+ operationName := OperationNameFromUrlPath("/projects", "OPTIONS")
+ assert.Equal(t, "projects.read", operationName)
+ })
+
+ t.Run("operation name fallback on unknown", func(t *testing.T) {
+ operationName := OperationNameFromUrlPath("/projects", "UNKNOWN")
+ assert.Equal(t, "projects.read", operationName)
+ })
+}
+
+func Test_OperationNameFromGrpcMethod(t *testing.T) {
+
+ t.Run("empty path", func(t *testing.T) {
+ operationName := OperationNameFromGrpcMethod("")
+ assert.Equal(t, "", operationName)
+ })
+
+ t.Run("root path", func(t *testing.T) {
+ operationName := OperationNameFromGrpcMethod("/")
+ assert.Equal(t, "", operationName)
+ })
+
+ t.Run("path without version", func(t *testing.T) {
+ operationName := OperationNameFromGrpcMethod("/example.ExampleService/ManualAuditEvent")
+ assert.Equal(t, "example.exampleservice.manualauditevent", operationName)
+ })
+
+ t.Run("path with version", func(t *testing.T) {
+ operationName := OperationNameFromGrpcMethod("/example.v1.ExampleService/ManualAuditEvent")
+ assert.Equal(t, "example.v1.exampleservice.manualauditevent", operationName)
+ })
+
+ t.Run("path trailing slash", func(t *testing.T) {
+ operationName := OperationNameFromGrpcMethod("/example.v1.ExampleService/ManualAuditEvent/")
+ assert.Equal(t, "example.v1.exampleservice.manualauditevent", operationName)
+ })
+}
+
+func Test_GetObjectIdAndTypeFromUrlPath(t *testing.T) {
+
+ t.Run("object id and type not in url", func(t *testing.T) {
+ objectId, objectType, err := GetObjectIdAndTypeFromUrlPath("/v2/projects/audit")
+ assert.NoError(t, err)
+ assert.Equal(t, "", objectId)
+ assert.Nil(t, objectType)
+ })
+
+ t.Run("object id and type in url", func(t *testing.T) {
+ objectId, objectType, err := GetObjectIdAndTypeFromUrlPath("/v2/projects/f17d4064-9b65-4334-b6a7-8fed96340124")
+ assert.NoError(t, err)
+ assert.Equal(t, "f17d4064-9b65-4334-b6a7-8fed96340124", objectId)
+ assert.Equal(t, ObjectTypeProject, *objectType)
+ })
+
+ t.Run("multiple object ids and types in url", func(t *testing.T) {
+ objectId, objectType, err := GetObjectIdAndTypeFromUrlPath("/v2/organization/8ee58bec-d496-4bb9-af8d-72fda4d78b6b/projects/f17d4064-9b65-4334-b6a7-8fed96340124")
+ assert.NoError(t, err)
+ assert.Equal(t, "f17d4064-9b65-4334-b6a7-8fed96340124", objectId)
+ assert.Equal(t, ObjectTypeProject, *objectType)
+ })
+}
+
+func Test_ToArrayMap(t *testing.T) {
+
+ t.Run("empty map", func(t *testing.T) {
+ result := ToArrayMap(map[string]string{})
+ assert.Equal(t, map[string][]string{}, result)
+ })
+
+ t.Run("empty map", func(t *testing.T) {
+ result := ToArrayMap(map[string]string{"key1": "value1", "key2": "value2"})
+ assert.Equal(t, map[string][]string{
+ "key1": {"value1"},
+ "key2": {"value2"},
+ }, result)
+ })
+}
+
+func Test_StringAttributeFromMetadata(t *testing.T) {
+
+ metadata := map[string][]string{"key1": {"value1"}, "key2": {"value2"}}
+
+ t.Run("not found", func(t *testing.T) {
+ attribute := StringAttributeFromMetadata(metadata, "key3")
+ assert.Equal(t, "", attribute)
+ })
+
+ t.Run("found", func(t *testing.T) {
+ attribute := StringAttributeFromMetadata(metadata, "key2")
+ assert.Equal(t, "value2", attribute)
+ })
+}
+
+func Test_ResponseBodyToBytes(t *testing.T) {
+
+ t.Run(
+ "nil response body", func(t *testing.T) {
+ bytes, err := ResponseBodyToBytes(nil)
+ assert.Nil(t, bytes)
+ assert.Nil(t, err)
+ },
+ )
+
+ t.Run(
+ "bytes", func(t *testing.T) {
+ responseBody := []byte("data")
+ bytes, err := ResponseBodyToBytes(responseBody)
+ assert.Nil(t, err)
+ assert.Equal(t, &responseBody, bytes)
+ },
+ )
+
+ t.Run(
+ "Protobuf message", func(t *testing.T) {
+ protobufMessage := auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: string(ObjectTypeProject)}
+ bytes, err := ResponseBodyToBytes(&protobufMessage)
+ assert.Nil(t, err)
+
+ expected, err := protojson.Marshal(&protobufMessage)
+ assert.Nil(t, err)
+ assert.Equal(t, &expected, bytes)
+ },
+ )
+
+ t.Run(
+ "struct", func(t *testing.T) {
+ type CustomObject struct {
+ Value string
+ }
+
+ responseBody := CustomObject{Value: "data"}
+ bytes, err := ResponseBodyToBytes(responseBody)
+ assert.Nil(t, err)
+
+ expected, err := json.Marshal(responseBody)
+ assert.Nil(t, err)
+ assert.Equal(t, &expected, bytes)
+ },
+ )
+
+ t.Run(
+ "map", func(t *testing.T) {
+
+ responseBody := map[string]interface{}{"value": "data"}
+ bytes, err := ResponseBodyToBytes(responseBody)
+ assert.Nil(t, err)
+
+ expected, err := json.Marshal(responseBody)
+ assert.Nil(t, err)
+ assert.Equal(t, &expected, bytes)
+ },
+ )
+}
diff --git a/audit/api/schema_validation_test.go b/audit/api/schema_validation_test.go
new file mode 100644
index 0000000..c0b141e
--- /dev/null
+++ b/audit/api/schema_validation_test.go
@@ -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]")
+ })
+}
diff --git a/audit/api/test_data.go b/audit/api/test_data.go
new file mode 100644
index 0000000..d981873
--- /dev/null
+++ b/audit/api/test_data.go
@@ -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
+}
diff --git a/audit/messaging/messaging.go b/audit/messaging/messaging.go
new file mode 100644
index 0000000..5e0f977
--- /dev/null
+++ b/audit/messaging/messaging.go
@@ -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()
+}
diff --git a/audit/messaging/messaging_test.go b/audit/messaging/messaging_test.go
new file mode 100644
index 0000000..973b17f
--- /dev/null
+++ b/audit/messaging/messaging_test.go
@@ -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)
+ })
+}
diff --git a/audit/messaging/solace.go b/audit/messaging/solace.go
new file mode 100644
index 0000000..b9b6fe8
--- /dev/null
+++ b/audit/messaging/solace.go
@@ -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()
+ }
+}
diff --git a/audit/utils/sequence_generator.go b/audit/utils/sequence_generator.go
new file mode 100644
index 0000000..ebf16f2
--- /dev/null
+++ b/audit/utils/sequence_generator.go
@@ -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
+}
diff --git a/audit/utils/sequence_generator_test.go b/audit/utils/sequence_generator_test.go
new file mode 100644
index 0000000..08fa08c
--- /dev/null
+++ b/audit/utils/sequence_generator_test.go
@@ -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())
+ })
+}
diff --git a/buf.lock b/buf.lock
new file mode 100644
index 0000000..c91b581
--- /dev/null
+++ b/buf.lock
@@ -0,0 +1,2 @@
+# Generated by buf. DO NOT EDIT.
+version: v1
diff --git a/gen/go/audit/v1/audit_event.pb.go b/gen/go/audit/v1/audit_event.pb.go
new file mode 100644
index 0000000..4111ce4
--- /dev/null
+++ b/gen/go/audit/v1/audit_event.pb.go
@@ -0,0 +1,1954 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.35.1
+// protoc (unknown)
+// source: audit/v1/audit_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"
+ structpb "google.golang.org/protobuf/types/known/structpb"
+ timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+ wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
+ 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)
+)
+
+// The severity of the event described in a log entry, expressed as one of the
+// standard severity levels listed below.
+type LogSeverity int32
+
+const (
+ LogSeverity_LOG_SEVERITY_UNSPECIFIED LogSeverity = 0
+ // The log entry has no assigned severity level.
+ LogSeverity_LOG_SEVERITY_DEFAULT LogSeverity = 100
+ // Debug or trace information.
+ LogSeverity_LOG_SEVERITY_DEBUG LogSeverity = 200
+ // Routine information, such as ongoing status or performance.
+ LogSeverity_LOG_SEVERITY_INFO LogSeverity = 300
+ // Normal but significant events, such as start up, shut down, or
+ // a configuration change.
+ LogSeverity_LOG_SEVERITY_NOTICE LogSeverity = 400
+ // Warning events might cause problems.
+ LogSeverity_LOG_SEVERITY_WARNING LogSeverity = 500
+ // Error events are likely to cause problems.
+ LogSeverity_LOG_SEVERITY_ERROR LogSeverity = 600
+ // Critical events cause more severe problems or outages.
+ LogSeverity_LOG_SEVERITY_CRITICAL LogSeverity = 700
+ // A person must take an action immediately.
+ LogSeverity_LOG_SEVERITY_ALERT LogSeverity = 800
+ // One or more systems are unusable.
+ LogSeverity_LOG_SEVERITY_EMERGENCY LogSeverity = 900
+)
+
+// Enum value maps for LogSeverity.
+var (
+ LogSeverity_name = map[int32]string{
+ 0: "LOG_SEVERITY_UNSPECIFIED",
+ 100: "LOG_SEVERITY_DEFAULT",
+ 200: "LOG_SEVERITY_DEBUG",
+ 300: "LOG_SEVERITY_INFO",
+ 400: "LOG_SEVERITY_NOTICE",
+ 500: "LOG_SEVERITY_WARNING",
+ 600: "LOG_SEVERITY_ERROR",
+ 700: "LOG_SEVERITY_CRITICAL",
+ 800: "LOG_SEVERITY_ALERT",
+ 900: "LOG_SEVERITY_EMERGENCY",
+ }
+ LogSeverity_value = map[string]int32{
+ "LOG_SEVERITY_UNSPECIFIED": 0,
+ "LOG_SEVERITY_DEFAULT": 100,
+ "LOG_SEVERITY_DEBUG": 200,
+ "LOG_SEVERITY_INFO": 300,
+ "LOG_SEVERITY_NOTICE": 400,
+ "LOG_SEVERITY_WARNING": 500,
+ "LOG_SEVERITY_ERROR": 600,
+ "LOG_SEVERITY_CRITICAL": 700,
+ "LOG_SEVERITY_ALERT": 800,
+ "LOG_SEVERITY_EMERGENCY": 900,
+ }
+)
+
+func (x LogSeverity) Enum() *LogSeverity {
+ p := new(LogSeverity)
+ *p = x
+ return p
+}
+
+func (x LogSeverity) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (LogSeverity) Descriptor() protoreflect.EnumDescriptor {
+ return file_audit_v1_audit_event_proto_enumTypes[0].Descriptor()
+}
+
+func (LogSeverity) Type() protoreflect.EnumType {
+ return &file_audit_v1_audit_event_proto_enumTypes[0]
+}
+
+func (x LogSeverity) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use LogSeverity.Descriptor instead.
+func (LogSeverity) EnumDescriptor() ([]byte, []int) {
+ return file_audit_v1_audit_event_proto_rawDescGZIP(), []int{0}
+}
+
+type AttributeContext_HttpMethod int32
+
+const (
+ AttributeContext_HTTP_METHOD_UNSPECIFIED AttributeContext_HttpMethod = 0
+ AttributeContext_HTTP_METHOD_OTHER AttributeContext_HttpMethod = 1
+ AttributeContext_HTTP_METHOD_GET AttributeContext_HttpMethod = 2
+ AttributeContext_HTTP_METHOD_HEAD AttributeContext_HttpMethod = 3
+ AttributeContext_HTTP_METHOD_POST AttributeContext_HttpMethod = 4
+ AttributeContext_HTTP_METHOD_PUT AttributeContext_HttpMethod = 5
+ AttributeContext_HTTP_METHOD_DELETE AttributeContext_HttpMethod = 6
+ AttributeContext_HTTP_METHOD_CONNECT AttributeContext_HttpMethod = 7
+ AttributeContext_HTTP_METHOD_OPTIONS AttributeContext_HttpMethod = 8
+ AttributeContext_HTTP_METHOD_TRACE AttributeContext_HttpMethod = 9
+ AttributeContext_HTTP_METHOD_PATCH AttributeContext_HttpMethod = 10
+)
+
+// Enum value maps for AttributeContext_HttpMethod.
+var (
+ AttributeContext_HttpMethod_name = map[int32]string{
+ 0: "HTTP_METHOD_UNSPECIFIED",
+ 1: "HTTP_METHOD_OTHER",
+ 2: "HTTP_METHOD_GET",
+ 3: "HTTP_METHOD_HEAD",
+ 4: "HTTP_METHOD_POST",
+ 5: "HTTP_METHOD_PUT",
+ 6: "HTTP_METHOD_DELETE",
+ 7: "HTTP_METHOD_CONNECT",
+ 8: "HTTP_METHOD_OPTIONS",
+ 9: "HTTP_METHOD_TRACE",
+ 10: "HTTP_METHOD_PATCH",
+ }
+ AttributeContext_HttpMethod_value = map[string]int32{
+ "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,
+ }
+)
+
+func (x AttributeContext_HttpMethod) Enum() *AttributeContext_HttpMethod {
+ p := new(AttributeContext_HttpMethod)
+ *p = x
+ return p
+}
+
+func (x AttributeContext_HttpMethod) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (AttributeContext_HttpMethod) Descriptor() protoreflect.EnumDescriptor {
+ return file_audit_v1_audit_event_proto_enumTypes[1].Descriptor()
+}
+
+func (AttributeContext_HttpMethod) Type() protoreflect.EnumType {
+ return &file_audit_v1_audit_event_proto_enumTypes[1]
+}
+
+func (x AttributeContext_HttpMethod) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use AttributeContext_HttpMethod.Descriptor instead.
+func (AttributeContext_HttpMethod) EnumDescriptor() ([]byte, []int) {
+ return file_audit_v1_audit_event_proto_rawDescGZIP(), []int{4, 0}
+}
+
+// The audit log entry can be used to record an incident in the audit log.
+type AuditLogEntry struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // The resource name of the log to which this log entry belongs.
+ //
+ // Format: //logs/
+ // 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
+ LogName string `protobuf:"bytes,1,opt,name=log_name,json=logName,proto3" json:"log_name,omitempty"`
+ // The log entry payload, which is always an AuditLog for STACKIT Audit Log events.
+ //
+ // Required: true
+ ProtoPayload *AuditLog `protobuf:"bytes,2,opt,name=proto_payload,json=protoPayload,proto3" json:"proto_payload,omitempty"`
+ // A unique identifier for the log entry.
+ // Is used to check completeness of audit events over time.
+ //
+ // Format: ///
+ // 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
+ InsertId string `protobuf:"bytes,3,opt,name=insert_id,json=insertId,proto3" json:"insert_id,omitempty"`
+ // A set of user-defined (key, value) data that provides additional
+ // information about the log entry.
+ //
+ // Required: false
+ Labels map[string]string `protobuf:"bytes,4,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+ // Correlate multiple audit logs by setting the same id
+ //
+ // Required: false
+ CorrelationId *string `protobuf:"bytes,5,opt,name=correlation_id,json=correlationId,proto3,oneof" json:"correlation_id,omitempty"`
+ // The time the event described by the log entry occurred.
+ //
+ // Required: true
+ Timestamp *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
+ // The severity of the log entry.
+ //
+ // Required: true
+ Severity LogSeverity `protobuf:"varint,7,opt,name=severity,proto3,enum=audit.v1.LogSeverity" json:"severity,omitempty"`
+ // Customer set W3C conform trace parent header:
+ // https://www.w3.org/TR/trace-context/#traceparent-header
+ //
+ // Format: ---
+ //
+ // Examples:
+ //
+ // "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
+ //
+ // Required: false
+ TraceParent *string `protobuf:"bytes,8,opt,name=trace_parent,json=traceParent,proto3,oneof" json:"trace_parent,omitempty"`
+ // Customer set W3C conform trace state header:
+ // https://www.w3.org/TR/trace-context/#tracestate-header
+ //
+ // Format: =[,=]
+ //
+ // Examples:
+ //
+ // "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"
+ //
+ // Required: false
+ TraceState *string `protobuf:"bytes,9,opt,name=trace_state,json=traceState,proto3,oneof" json:"trace_state,omitempty"`
+}
+
+func (x *AuditLogEntry) Reset() {
+ *x = AuditLogEntry{}
+ mi := &file_audit_v1_audit_event_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AuditLogEntry) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AuditLogEntry) ProtoMessage() {}
+
+func (x *AuditLogEntry) ProtoReflect() protoreflect.Message {
+ mi := &file_audit_v1_audit_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 AuditLogEntry.ProtoReflect.Descriptor instead.
+func (*AuditLogEntry) Descriptor() ([]byte, []int) {
+ return file_audit_v1_audit_event_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *AuditLogEntry) GetLogName() string {
+ if x != nil {
+ return x.LogName
+ }
+ return ""
+}
+
+func (x *AuditLogEntry) GetProtoPayload() *AuditLog {
+ if x != nil {
+ return x.ProtoPayload
+ }
+ return nil
+}
+
+func (x *AuditLogEntry) GetInsertId() string {
+ if x != nil {
+ return x.InsertId
+ }
+ return ""
+}
+
+func (x *AuditLogEntry) GetLabels() map[string]string {
+ if x != nil {
+ return x.Labels
+ }
+ return nil
+}
+
+func (x *AuditLogEntry) GetCorrelationId() string {
+ if x != nil && x.CorrelationId != nil {
+ return *x.CorrelationId
+ }
+ return ""
+}
+
+func (x *AuditLogEntry) GetTimestamp() *timestamppb.Timestamp {
+ if x != nil {
+ return x.Timestamp
+ }
+ return nil
+}
+
+func (x *AuditLogEntry) GetSeverity() LogSeverity {
+ if x != nil {
+ return x.Severity
+ }
+ return LogSeverity_LOG_SEVERITY_UNSPECIFIED
+}
+
+func (x *AuditLogEntry) GetTraceParent() string {
+ if x != nil && x.TraceParent != nil {
+ return *x.TraceParent
+ }
+ return ""
+}
+
+func (x *AuditLogEntry) GetTraceState() string {
+ if x != nil && x.TraceState != nil {
+ return *x.TraceState
+ }
+ return ""
+}
+
+// Common audit log format for STACKIT API operations.
+type AuditLog struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // The name of the API service performing the operation.
+ //
+ // Examples:
+ //
+ // "resource-manager"
+ //
+ // Required: true
+ ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"`
+ // The name of the service method or operation.
+ //
+ // Format: stackit....
+ // 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,2,opt,name=operation_name,json=operationName,proto3" json:"operation_name,omitempty"`
+ // 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: /[/]
+ // Where:
+ //
+ // Plural-Type: One from the list of supported ObjectType as plural
+ // Id: The identifier of the object
+ // Details: Optional "/" 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
+ ResourceName string `protobuf:"bytes,3,opt,name=resource_name,json=resourceName,proto3" json:"resource_name,omitempty"`
+ // Authentication information.
+ //
+ // Required: true
+ AuthenticationInfo *AuthenticationInfo `protobuf:"bytes,4,opt,name=authentication_info,json=authenticationInfo,proto3" json:"authentication_info,omitempty"`
+ // Authorization information. If there are multiple resources or permissions involved, then there is
+ // one AuthorizationInfo element for each {resource, permission} tuple.
+ //
+ // Required: false
+ AuthorizationInfo []*AuthorizationInfo `protobuf:"bytes,5,rep,name=authorization_info,json=authorizationInfo,proto3" json:"authorization_info,omitempty"`
+ // Metadata about the operation.
+ //
+ // Required: true
+ RequestMetadata *RequestMetadata `protobuf:"bytes,6,opt,name=request_metadata,json=requestMetadata,proto3" json:"request_metadata,omitempty"`
+ // 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
+ Request *structpb.Struct `protobuf:"bytes,7,opt,name=request,proto3,oneof" json:"request,omitempty"`
+ // The status of the overall operation.
+ //
+ // Required: true
+ ResponseMetadata *ResponseMetadata `protobuf:"bytes,8,opt,name=response_metadata,json=responseMetadata,proto3" json:"response_metadata,omitempty"`
+ // 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
+ Response *structpb.Struct `protobuf:"bytes,9,opt,name=response,proto3,oneof" json:"response,omitempty"`
+ // Other service-specific data about the request, response, and other
+ // information associated with the current audited event.
+ //
+ // Required: false
+ Metadata *structpb.Struct `protobuf:"bytes,10,opt,name=metadata,proto3,oneof" json:"metadata,omitempty"`
+}
+
+func (x *AuditLog) Reset() {
+ *x = AuditLog{}
+ mi := &file_audit_v1_audit_event_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AuditLog) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AuditLog) ProtoMessage() {}
+
+func (x *AuditLog) ProtoReflect() protoreflect.Message {
+ mi := &file_audit_v1_audit_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 AuditLog.ProtoReflect.Descriptor instead.
+func (*AuditLog) Descriptor() ([]byte, []int) {
+ return file_audit_v1_audit_event_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *AuditLog) GetServiceName() string {
+ if x != nil {
+ return x.ServiceName
+ }
+ return ""
+}
+
+func (x *AuditLog) GetOperationName() string {
+ if x != nil {
+ return x.OperationName
+ }
+ return ""
+}
+
+func (x *AuditLog) GetResourceName() string {
+ if x != nil {
+ return x.ResourceName
+ }
+ return ""
+}
+
+func (x *AuditLog) GetAuthenticationInfo() *AuthenticationInfo {
+ if x != nil {
+ return x.AuthenticationInfo
+ }
+ return nil
+}
+
+func (x *AuditLog) GetAuthorizationInfo() []*AuthorizationInfo {
+ if x != nil {
+ return x.AuthorizationInfo
+ }
+ return nil
+}
+
+func (x *AuditLog) GetRequestMetadata() *RequestMetadata {
+ if x != nil {
+ return x.RequestMetadata
+ }
+ return nil
+}
+
+func (x *AuditLog) GetRequest() *structpb.Struct {
+ if x != nil {
+ return x.Request
+ }
+ return nil
+}
+
+func (x *AuditLog) GetResponseMetadata() *ResponseMetadata {
+ if x != nil {
+ return x.ResponseMetadata
+ }
+ return nil
+}
+
+func (x *AuditLog) GetResponse() *structpb.Struct {
+ if x != nil {
+ return x.Response
+ }
+ return nil
+}
+
+func (x *AuditLog) GetMetadata() *structpb.Struct {
+ if x != nil {
+ return x.Metadata
+ }
+ return nil
+}
+
+// Authentication information for the operation.
+type AuthenticationInfo struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // STACKIT principal id
+ //
+ // Required: true
+ PrincipalId string `protobuf:"bytes,1,opt,name=principal_id,json=principalId,proto3" json:"principal_id,omitempty"`
+ // The email address of the authenticated user.
+ // Service accounts have email addresses that can be used.
+ //
+ // Required: true
+ PrincipalEmail string `protobuf:"bytes,2,opt,name=principal_email,json=principalEmail,proto3" json:"principal_email,omitempty"`
+ // The name of the service account used to create or exchange
+ // credentials for authenticating the service account making the request.
+ //
+ // Format: projects//service-accounts/
+ //
+ // Examples:
+ //
+ // "projects/29b2c56f-f712-4a9c-845b-f0907158e53c/service-accounts/a606dc68-8b97-421b-89a9-116bcbd004df"
+ //
+ // Required: false
+ ServiceAccountName *string `protobuf:"bytes,3,opt,name=service_account_name,json=serviceAccountName,proto3,oneof" json:"service_account_name,omitempty"`
+ // 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
+ ServiceAccountDelegationInfo []*ServiceAccountDelegationInfo `protobuf:"bytes,4,rep,name=service_account_delegation_info,json=serviceAccountDelegationInfo,proto3" json:"service_account_delegation_info,omitempty"`
+}
+
+func (x *AuthenticationInfo) Reset() {
+ *x = AuthenticationInfo{}
+ mi := &file_audit_v1_audit_event_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AuthenticationInfo) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AuthenticationInfo) ProtoMessage() {}
+
+func (x *AuthenticationInfo) ProtoReflect() protoreflect.Message {
+ mi := &file_audit_v1_audit_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 AuthenticationInfo.ProtoReflect.Descriptor instead.
+func (*AuthenticationInfo) Descriptor() ([]byte, []int) {
+ return file_audit_v1_audit_event_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *AuthenticationInfo) GetPrincipalId() string {
+ if x != nil {
+ return x.PrincipalId
+ }
+ return ""
+}
+
+func (x *AuthenticationInfo) GetPrincipalEmail() string {
+ if x != nil {
+ return x.PrincipalEmail
+ }
+ return ""
+}
+
+func (x *AuthenticationInfo) GetServiceAccountName() string {
+ if x != nil && x.ServiceAccountName != nil {
+ return *x.ServiceAccountName
+ }
+ return ""
+}
+
+func (x *AuthenticationInfo) GetServiceAccountDelegationInfo() []*ServiceAccountDelegationInfo {
+ if x != nil {
+ return x.ServiceAccountDelegationInfo
+ }
+ return nil
+}
+
+// Authorization information for the operation.
+type AuthorizationInfo struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // The resource being accessed, as a REST-style string.
+ //
+ // Format: /[/]
+ // Where:
+ //
+ // Plural-Type: One from the list of supported ObjectType as plural
+ // Id: The identifier of the object
+ // Details: Optional "/" 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
+ Resource string `protobuf:"bytes,1,opt,name=resource,proto3" json:"resource,omitempty"`
+ // The required IAM permission.
+ //
+ // Examples:
+ //
+ // "resourcemanager.project.edit"
+ //
+ // Required: false
+ Permission *string `protobuf:"bytes,2,opt,name=permission,proto3,oneof" json:"permission,omitempty"`
+ // IAM permission check result.
+ //
+ // Required: false
+ Granted *bool `protobuf:"varint,3,opt,name=granted,proto3,oneof" json:"granted,omitempty"`
+}
+
+func (x *AuthorizationInfo) Reset() {
+ *x = AuthorizationInfo{}
+ mi := &file_audit_v1_audit_event_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AuthorizationInfo) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AuthorizationInfo) ProtoMessage() {}
+
+func (x *AuthorizationInfo) ProtoReflect() protoreflect.Message {
+ mi := &file_audit_v1_audit_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 AuthorizationInfo.ProtoReflect.Descriptor instead.
+func (*AuthorizationInfo) Descriptor() ([]byte, []int) {
+ return file_audit_v1_audit_event_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *AuthorizationInfo) GetResource() string {
+ if x != nil {
+ return x.Resource
+ }
+ return ""
+}
+
+func (x *AuthorizationInfo) GetPermission() string {
+ if x != nil && x.Permission != nil {
+ return *x.Permission
+ }
+ return ""
+}
+
+func (x *AuthorizationInfo) GetGranted() bool {
+ if x != nil && x.Granted != nil {
+ return *x.Granted
+ }
+ return false
+}
+
+// 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.
+type AttributeContext struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *AttributeContext) Reset() {
+ *x = AttributeContext{}
+ mi := &file_audit_v1_audit_event_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AttributeContext) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AttributeContext) ProtoMessage() {}
+
+func (x *AttributeContext) ProtoReflect() protoreflect.Message {
+ mi := &file_audit_v1_audit_event_proto_msgTypes[4]
+ 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 AttributeContext.ProtoReflect.Descriptor instead.
+func (*AttributeContext) Descriptor() ([]byte, []int) {
+ return file_audit_v1_audit_event_proto_rawDescGZIP(), []int{4}
+}
+
+// Metadata about the request.
+type RequestMetadata struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // 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
+ CallerIp string `protobuf:"bytes,1,opt,name=caller_ip,json=callerIp,proto3" json:"caller_ip,omitempty"`
+ // 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
+ CallerSuppliedUserAgent string `protobuf:"bytes,2,opt,name=caller_supplied_user_agent,json=callerSuppliedUserAgent,proto3" json:"caller_supplied_user_agent,omitempty"`
+ // This field contains request attributes like request url, time, etc.
+ //
+ // Required: true
+ RequestAttributes *AttributeContext_Request `protobuf:"bytes,3,opt,name=request_attributes,json=requestAttributes,proto3" json:"request_attributes,omitempty"`
+}
+
+func (x *RequestMetadata) Reset() {
+ *x = RequestMetadata{}
+ mi := &file_audit_v1_audit_event_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *RequestMetadata) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RequestMetadata) ProtoMessage() {}
+
+func (x *RequestMetadata) ProtoReflect() protoreflect.Message {
+ mi := &file_audit_v1_audit_event_proto_msgTypes[5]
+ 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 RequestMetadata.ProtoReflect.Descriptor instead.
+func (*RequestMetadata) Descriptor() ([]byte, []int) {
+ return file_audit_v1_audit_event_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *RequestMetadata) GetCallerIp() string {
+ if x != nil {
+ return x.CallerIp
+ }
+ return ""
+}
+
+func (x *RequestMetadata) GetCallerSuppliedUserAgent() string {
+ if x != nil {
+ return x.CallerSuppliedUserAgent
+ }
+ return ""
+}
+
+func (x *RequestMetadata) GetRequestAttributes() *AttributeContext_Request {
+ if x != nil {
+ return x.RequestAttributes
+ }
+ return nil
+}
+
+// Metadata about the response
+type ResponseMetadata struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // 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
+ StatusCode *wrapperspb.Int32Value `protobuf:"bytes,1,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"`
+ // Short description of the error
+ //
+ // Required: false
+ ErrorMessage *string `protobuf:"bytes,2,opt,name=error_message,json=errorMessage,proto3,oneof" json:"error_message,omitempty"`
+ // Error details
+ //
+ // Required: false
+ ErrorDetails []*structpb.Struct `protobuf:"bytes,3,rep,name=error_details,json=errorDetails,proto3" json:"error_details,omitempty"`
+ // This field contains response attributes like headers, time, etc.
+ //
+ // Required: true
+ ResponseAttributes *AttributeContext_Response `protobuf:"bytes,4,opt,name=response_attributes,json=responseAttributes,proto3" json:"response_attributes,omitempty"`
+}
+
+func (x *ResponseMetadata) Reset() {
+ *x = ResponseMetadata{}
+ mi := &file_audit_v1_audit_event_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ResponseMetadata) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ResponseMetadata) ProtoMessage() {}
+
+func (x *ResponseMetadata) ProtoReflect() protoreflect.Message {
+ mi := &file_audit_v1_audit_event_proto_msgTypes[6]
+ 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 ResponseMetadata.ProtoReflect.Descriptor instead.
+func (*ResponseMetadata) Descriptor() ([]byte, []int) {
+ return file_audit_v1_audit_event_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *ResponseMetadata) GetStatusCode() *wrapperspb.Int32Value {
+ if x != nil {
+ return x.StatusCode
+ }
+ return nil
+}
+
+func (x *ResponseMetadata) GetErrorMessage() string {
+ if x != nil && x.ErrorMessage != nil {
+ return *x.ErrorMessage
+ }
+ return ""
+}
+
+func (x *ResponseMetadata) GetErrorDetails() []*structpb.Struct {
+ if x != nil {
+ return x.ErrorDetails
+ }
+ return nil
+}
+
+func (x *ResponseMetadata) GetResponseAttributes() *AttributeContext_Response {
+ if x != nil {
+ return x.ResponseAttributes
+ }
+ return nil
+}
+
+// Identity delegation history of an authenticated service account.
+type ServiceAccountDelegationInfo struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // Entity that creates credentials for service account and assumes its
+ // identity for authentication.
+ //
+ // Types that are assignable to Authority:
+ //
+ // *ServiceAccountDelegationInfo_SystemPrincipal_
+ // *ServiceAccountDelegationInfo_IdpPrincipal_
+ Authority isServiceAccountDelegationInfo_Authority `protobuf_oneof:"authority"`
+}
+
+func (x *ServiceAccountDelegationInfo) Reset() {
+ *x = ServiceAccountDelegationInfo{}
+ mi := &file_audit_v1_audit_event_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ServiceAccountDelegationInfo) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ServiceAccountDelegationInfo) ProtoMessage() {}
+
+func (x *ServiceAccountDelegationInfo) ProtoReflect() protoreflect.Message {
+ mi := &file_audit_v1_audit_event_proto_msgTypes[7]
+ 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 ServiceAccountDelegationInfo.ProtoReflect.Descriptor instead.
+func (*ServiceAccountDelegationInfo) Descriptor() ([]byte, []int) {
+ return file_audit_v1_audit_event_proto_rawDescGZIP(), []int{7}
+}
+
+func (m *ServiceAccountDelegationInfo) GetAuthority() isServiceAccountDelegationInfo_Authority {
+ if m != nil {
+ return m.Authority
+ }
+ return nil
+}
+
+func (x *ServiceAccountDelegationInfo) GetSystemPrincipal() *ServiceAccountDelegationInfo_SystemPrincipal {
+ if x, ok := x.GetAuthority().(*ServiceAccountDelegationInfo_SystemPrincipal_); ok {
+ return x.SystemPrincipal
+ }
+ return nil
+}
+
+func (x *ServiceAccountDelegationInfo) GetIdpPrincipal() *ServiceAccountDelegationInfo_IdpPrincipal {
+ if x, ok := x.GetAuthority().(*ServiceAccountDelegationInfo_IdpPrincipal_); ok {
+ return x.IdpPrincipal
+ }
+ return nil
+}
+
+type isServiceAccountDelegationInfo_Authority interface {
+ isServiceAccountDelegationInfo_Authority()
+}
+
+type ServiceAccountDelegationInfo_SystemPrincipal_ struct {
+ // System identity
+ SystemPrincipal *ServiceAccountDelegationInfo_SystemPrincipal `protobuf:"bytes,1,opt,name=system_principal,json=systemPrincipal,proto3,oneof"`
+}
+
+type ServiceAccountDelegationInfo_IdpPrincipal_ struct {
+ // STACKIT IDP identity
+ IdpPrincipal *ServiceAccountDelegationInfo_IdpPrincipal `protobuf:"bytes,2,opt,name=idp_principal,json=idpPrincipal,proto3,oneof"`
+}
+
+func (*ServiceAccountDelegationInfo_SystemPrincipal_) isServiceAccountDelegationInfo_Authority() {}
+
+func (*ServiceAccountDelegationInfo_IdpPrincipal_) isServiceAccountDelegationInfo_Authority() {}
+
+// 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.
+type AttributeContext_Auth struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // The authenticated principal. Reflects the issuer ("iss") and subject
+ // ("sub") claims within a JWT.
+ //
+ // Format: /
+ // 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
+ Principal string `protobuf:"bytes,1,opt,name=principal,proto3" json:"principal,omitempty"`
+ // 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
+ Audiences []string `protobuf:"bytes,2,rep,name=audiences,proto3" json:"audiences,omitempty"`
+ // Structured claims presented with the credential. JWTs include
+ // {"key": } 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
+ Claims *structpb.Struct `protobuf:"bytes,3,opt,name=claims,proto3" json:"claims,omitempty"`
+}
+
+func (x *AttributeContext_Auth) Reset() {
+ *x = AttributeContext_Auth{}
+ mi := &file_audit_v1_audit_event_proto_msgTypes[9]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AttributeContext_Auth) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AttributeContext_Auth) ProtoMessage() {}
+
+func (x *AttributeContext_Auth) ProtoReflect() protoreflect.Message {
+ mi := &file_audit_v1_audit_event_proto_msgTypes[9]
+ 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 AttributeContext_Auth.ProtoReflect.Descriptor instead.
+func (*AttributeContext_Auth) Descriptor() ([]byte, []int) {
+ return file_audit_v1_audit_event_proto_rawDescGZIP(), []int{4, 0}
+}
+
+func (x *AttributeContext_Auth) GetPrincipal() string {
+ if x != nil {
+ return x.Principal
+ }
+ return ""
+}
+
+func (x *AttributeContext_Auth) GetAudiences() []string {
+ if x != nil {
+ return x.Audiences
+ }
+ return nil
+}
+
+func (x *AttributeContext_Auth) GetClaims() *structpb.Struct {
+ if x != nil {
+ return x.Claims
+ }
+ return nil
+}
+
+// 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.
+type AttributeContext_Request struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // 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:
+ // Where:
+ //
+ // Idempotency-key: Typically consists of a id + version
+ //
+ // Examples:
+ //
+ // 5e3952a9-b628-4be6-ac61-b1c6eb4a110c/5
+ //
+ // Required: false
+ Id *string `protobuf:"bytes,1,opt,name=id,proto3,oneof" json:"id,omitempty"`
+ // The (HTTP) request method, such as `GET`, `POST`.
+ //
+ // Required: true
+ Method AttributeContext_HttpMethod `protobuf:"varint,2,opt,name=method,proto3,enum=audit.v1.AttributeContext_HttpMethod" json:"method,omitempty"`
+ // 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
+ Headers map[string]string `protobuf:"bytes,3,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+ // The gRPC / HTTP URL path.
+ //
+ // Required: true
+ Path string `protobuf:"bytes,4,opt,name=path,proto3" json:"path,omitempty"`
+ // The HTTP request `Host` header value.
+ //
+ // Required: true
+ Host string `protobuf:"bytes,5,opt,name=host,proto3" json:"host,omitempty"`
+ // The URL scheme, such as `http`, `https` or `gRPC`.
+ //
+ // Required: true
+ Scheme string `protobuf:"bytes,6,opt,name=scheme,proto3" json:"scheme,omitempty"`
+ // 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
+ Query *string `protobuf:"bytes,7,opt,name=query,proto3,oneof" json:"query,omitempty"`
+ // The timestamp when the `destination` service receives the first byte of
+ // the request.
+ //
+ // Required: true
+ Time *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=time,proto3" json:"time,omitempty"`
+ // 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
+ Protocol string `protobuf:"bytes,9,opt,name=protocol,proto3" json:"protocol,omitempty"`
+ // The request authentication.
+ //
+ // Required: true
+ Auth *AttributeContext_Auth `protobuf:"bytes,10,opt,name=auth,proto3" json:"auth,omitempty"`
+}
+
+func (x *AttributeContext_Request) Reset() {
+ *x = AttributeContext_Request{}
+ mi := &file_audit_v1_audit_event_proto_msgTypes[10]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AttributeContext_Request) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AttributeContext_Request) ProtoMessage() {}
+
+func (x *AttributeContext_Request) ProtoReflect() protoreflect.Message {
+ mi := &file_audit_v1_audit_event_proto_msgTypes[10]
+ 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 AttributeContext_Request.ProtoReflect.Descriptor instead.
+func (*AttributeContext_Request) Descriptor() ([]byte, []int) {
+ return file_audit_v1_audit_event_proto_rawDescGZIP(), []int{4, 1}
+}
+
+func (x *AttributeContext_Request) GetId() string {
+ if x != nil && x.Id != nil {
+ return *x.Id
+ }
+ return ""
+}
+
+func (x *AttributeContext_Request) GetMethod() AttributeContext_HttpMethod {
+ if x != nil {
+ return x.Method
+ }
+ return AttributeContext_HTTP_METHOD_UNSPECIFIED
+}
+
+func (x *AttributeContext_Request) GetHeaders() map[string]string {
+ if x != nil {
+ return x.Headers
+ }
+ return nil
+}
+
+func (x *AttributeContext_Request) GetPath() string {
+ if x != nil {
+ return x.Path
+ }
+ return ""
+}
+
+func (x *AttributeContext_Request) GetHost() string {
+ if x != nil {
+ return x.Host
+ }
+ return ""
+}
+
+func (x *AttributeContext_Request) GetScheme() string {
+ if x != nil {
+ return x.Scheme
+ }
+ return ""
+}
+
+func (x *AttributeContext_Request) GetQuery() string {
+ if x != nil && x.Query != nil {
+ return *x.Query
+ }
+ return ""
+}
+
+func (x *AttributeContext_Request) GetTime() *timestamppb.Timestamp {
+ if x != nil {
+ return x.Time
+ }
+ return nil
+}
+
+func (x *AttributeContext_Request) GetProtocol() string {
+ if x != nil {
+ return x.Protocol
+ }
+ return ""
+}
+
+func (x *AttributeContext_Request) GetAuth() *AttributeContext_Auth {
+ if x != nil {
+ return x.Auth
+ }
+ return nil
+}
+
+// This message defines attributes for a typical network response. It
+// generally models semantics of an HTTP response.
+type AttributeContext_Response struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // The number of items returned to the client if applicable.
+ //
+ // Required: false
+ NumResponseItems *wrapperspb.Int64Value `protobuf:"bytes,1,opt,name=num_response_items,json=numResponseItems,proto3,oneof" json:"num_response_items,omitempty"`
+ // The HTTP response size in bytes.
+ //
+ // Required: false
+ Size *wrapperspb.Int64Value `protobuf:"bytes,2,opt,name=size,proto3,oneof" json:"size,omitempty"`
+ // 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
+ Headers map[string]string `protobuf:"bytes,3,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+ // The timestamp when the "destination" service generates the first byte of
+ // the response.
+ //
+ // Required: true
+ Time *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=time,proto3" json:"time,omitempty"`
+}
+
+func (x *AttributeContext_Response) Reset() {
+ *x = AttributeContext_Response{}
+ mi := &file_audit_v1_audit_event_proto_msgTypes[11]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AttributeContext_Response) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AttributeContext_Response) ProtoMessage() {}
+
+func (x *AttributeContext_Response) ProtoReflect() protoreflect.Message {
+ mi := &file_audit_v1_audit_event_proto_msgTypes[11]
+ 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 AttributeContext_Response.ProtoReflect.Descriptor instead.
+func (*AttributeContext_Response) Descriptor() ([]byte, []int) {
+ return file_audit_v1_audit_event_proto_rawDescGZIP(), []int{4, 2}
+}
+
+func (x *AttributeContext_Response) GetNumResponseItems() *wrapperspb.Int64Value {
+ if x != nil {
+ return x.NumResponseItems
+ }
+ return nil
+}
+
+func (x *AttributeContext_Response) GetSize() *wrapperspb.Int64Value {
+ if x != nil {
+ return x.Size
+ }
+ return nil
+}
+
+func (x *AttributeContext_Response) GetHeaders() map[string]string {
+ if x != nil {
+ return x.Headers
+ }
+ return nil
+}
+
+func (x *AttributeContext_Response) GetTime() *timestamppb.Timestamp {
+ if x != nil {
+ return x.Time
+ }
+ return nil
+}
+
+// Anonymous system principal to be used when no user identity is available.
+type ServiceAccountDelegationInfo_SystemPrincipal struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // Metadata about the service that uses the service account.
+ //
+ // Required: false
+ ServiceMetadata *structpb.Struct `protobuf:"bytes,1,opt,name=service_metadata,json=serviceMetadata,proto3,oneof" json:"service_metadata,omitempty"`
+}
+
+func (x *ServiceAccountDelegationInfo_SystemPrincipal) Reset() {
+ *x = ServiceAccountDelegationInfo_SystemPrincipal{}
+ mi := &file_audit_v1_audit_event_proto_msgTypes[14]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ServiceAccountDelegationInfo_SystemPrincipal) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ServiceAccountDelegationInfo_SystemPrincipal) ProtoMessage() {}
+
+func (x *ServiceAccountDelegationInfo_SystemPrincipal) ProtoReflect() protoreflect.Message {
+ mi := &file_audit_v1_audit_event_proto_msgTypes[14]
+ 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 ServiceAccountDelegationInfo_SystemPrincipal.ProtoReflect.Descriptor instead.
+func (*ServiceAccountDelegationInfo_SystemPrincipal) Descriptor() ([]byte, []int) {
+ return file_audit_v1_audit_event_proto_rawDescGZIP(), []int{7, 0}
+}
+
+func (x *ServiceAccountDelegationInfo_SystemPrincipal) GetServiceMetadata() *structpb.Struct {
+ if x != nil {
+ return x.ServiceMetadata
+ }
+ return nil
+}
+
+// STACKIT idp principal.
+type ServiceAccountDelegationInfo_IdpPrincipal struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // STACKIT principal id
+ //
+ // Required: true
+ PrincipalId string `protobuf:"bytes,1,opt,name=principal_id,json=principalId,proto3" json:"principal_id,omitempty"`
+ // The email address of the authenticated user.
+ // Service accounts have email addresses that can be used.
+ //
+ // Required: true
+ PrincipalEmail string `protobuf:"bytes,2,opt,name=principal_email,json=principalEmail,proto3" json:"principal_email,omitempty"`
+ // Metadata about the service that uses the service account.
+ //
+ // Required: false
+ ServiceMetadata *structpb.Struct `protobuf:"bytes,3,opt,name=service_metadata,json=serviceMetadata,proto3,oneof" json:"service_metadata,omitempty"`
+}
+
+func (x *ServiceAccountDelegationInfo_IdpPrincipal) Reset() {
+ *x = ServiceAccountDelegationInfo_IdpPrincipal{}
+ mi := &file_audit_v1_audit_event_proto_msgTypes[15]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ServiceAccountDelegationInfo_IdpPrincipal) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ServiceAccountDelegationInfo_IdpPrincipal) ProtoMessage() {}
+
+func (x *ServiceAccountDelegationInfo_IdpPrincipal) ProtoReflect() protoreflect.Message {
+ mi := &file_audit_v1_audit_event_proto_msgTypes[15]
+ 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 ServiceAccountDelegationInfo_IdpPrincipal.ProtoReflect.Descriptor instead.
+func (*ServiceAccountDelegationInfo_IdpPrincipal) Descriptor() ([]byte, []int) {
+ return file_audit_v1_audit_event_proto_rawDescGZIP(), []int{7, 1}
+}
+
+func (x *ServiceAccountDelegationInfo_IdpPrincipal) GetPrincipalId() string {
+ if x != nil {
+ return x.PrincipalId
+ }
+ return ""
+}
+
+func (x *ServiceAccountDelegationInfo_IdpPrincipal) GetPrincipalEmail() string {
+ if x != nil {
+ return x.PrincipalEmail
+ }
+ return ""
+}
+
+func (x *ServiceAccountDelegationInfo_IdpPrincipal) GetServiceMetadata() *structpb.Struct {
+ if x != nil {
+ return x.ServiceMetadata
+ }
+ return nil
+}
+
+var File_audit_v1_audit_event_proto protoreflect.FileDescriptor
+
+var file_audit_v1_audit_event_proto_rawDesc = []byte{
+ 0x0a, 0x1a, 0x61, 0x75, 0x64, 0x69, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x64, 0x69, 0x74,
+ 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, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74,
+ 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74,
+ 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
+ 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f,
+ 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f,
+ 0x74, 0x6f, 0x22, 0xfd, 0x05, 0x0a, 0x0d, 0x41, 0x75, 0x64, 0x69, 0x74, 0x4c, 0x6f, 0x67, 0x45,
+ 0x6e, 0x74, 0x72, 0x79, 0x12, 0x78, 0x0a, 0x08, 0x6c, 0x6f, 0x67, 0x5f, 0x6e, 0x61, 0x6d, 0x65,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x5d, 0xba, 0x48, 0x5a, 0xc8, 0x01, 0x01, 0x72, 0x55,
+ 0x32, 0x53, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x2d, 0x5d, 0x2b, 0x2f, 0x5b, 0x61, 0x2d, 0x7a, 0x30,
+ 0x2d, 0x39, 0x2d, 0x5d, 0x2b, 0x2f, 0x6c, 0x6f, 0x67, 0x73, 0x2f, 0x28, 0x3f, 0x3a, 0x61, 0x64,
+ 0x6d, 0x69, 0x6e, 0x2d, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x7c, 0x73, 0x79, 0x73,
+ 0x74, 0x65, 0x6d, 0x2d, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x7c, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79,
+ 0x2d, 0x64, 0x65, 0x6e, 0x69, 0x65, 0x64, 0x7c, 0x64, 0x61, 0x74, 0x61, 0x2d, 0x61, 0x63, 0x63,
+ 0x65, 0x73, 0x73, 0x29, 0x24, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x3f,
+ 0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31,
+ 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x4c, 0x6f, 0x67, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01,
+ 0x01, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12,
+ 0x4c, 0x0a, 0x09, 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01,
+ 0x28, 0x09, 0x42, 0x2f, 0xba, 0x48, 0x2c, 0xc8, 0x01, 0x01, 0x72, 0x27, 0x32, 0x25, 0x5e, 0x5b,
+ 0x30, 0x2d, 0x39, 0x5d, 0x2b, 0x2f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x2d, 0x5d, 0x2b,
+ 0x2f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x2d, 0x5d, 0x2b, 0x2f, 0x5b, 0x30, 0x2d, 0x39,
+ 0x5d, 0x2b, 0x24, 0x52, 0x08, 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x49, 0x64, 0x12, 0x3b, 0x0a,
+ 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e,
+ 0x61, 0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x4c, 0x6f,
+ 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74,
+ 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x36, 0x0a, 0x0e, 0x63, 0x6f,
+ 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01,
+ 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0xff, 0x01, 0x48, 0x00,
+ 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x88,
+ 0x01, 0x01, 0x12, 0x45, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18,
+ 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
+ 0x70, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0xb2, 0x01, 0x02, 0x38, 0x01, 0x52, 0x09,
+ 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3e, 0x0a, 0x08, 0x73, 0x65, 0x76,
+ 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x61, 0x75,
+ 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69,
+ 0x74, 0x79, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52,
+ 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x52, 0x0a, 0x0c, 0x74, 0x72, 0x61,
+ 0x63, 0x65, 0x5f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x42,
+ 0x2a, 0xba, 0x48, 0x27, 0x72, 0x25, 0x32, 0x23, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x5d, 0x2b, 0x2d,
+ 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x2b, 0x2d, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d,
+ 0x39, 0x5d, 0x2b, 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x5d, 0x2b, 0x24, 0x48, 0x01, 0x52, 0x0b, 0x74,
+ 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x24, 0x0a,
+ 0x0b, 0x74, 0x72, 0x61, 0x63, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x09, 0x20, 0x01,
+ 0x28, 0x09, 0x48, 0x02, 0x52, 0x0a, 0x74, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65,
+ 0x88, 0x01, 0x01, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74,
+ 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x11,
+ 0x0a, 0x0f, 0x5f, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69,
+ 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x74, 0x72, 0x61, 0x63, 0x65, 0x5f, 0x70, 0x61, 0x72, 0x65,
+ 0x6e, 0x74, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x74, 0x72, 0x61, 0x63, 0x65, 0x5f, 0x73, 0x74, 0x61,
+ 0x74, 0x65, 0x22, 0xab, 0x06, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x74, 0x4c, 0x6f, 0x67, 0x12,
+ 0x2d, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0xc8, 0x01, 0x01, 0x72, 0x02, 0x10,
+ 0x01, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x77,
+ 0x0a, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x50, 0xba, 0x48, 0x4d, 0xc8, 0x01, 0x01, 0x72, 0x48,
+ 0x10, 0x01, 0x18, 0xff, 0x01, 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, 0x63, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75,
+ 0x72, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x3e,
+ 0xba, 0x48, 0x3b, 0xc8, 0x01, 0x01, 0x72, 0x36, 0x10, 0x01, 0x18, 0xff, 0x01, 0x32, 0x2f, 0x5e,
+ 0x5b, 0x61, 0x2d, 0x7a, 0x5d, 0x2b, 0x2f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x2d, 0x5d,
+ 0x2b, 0x28, 0x3f, 0x3a, 0x2f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x2d, 0x5d, 0x2b, 0x2f,
+ 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x2d, 0x5f, 0x5d, 0x2b, 0x29, 0x2a, 0x24, 0x52, 0x0c,
+ 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x55, 0x0a, 0x13,
+ 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69,
+ 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x64, 0x69,
+ 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74,
+ 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52,
+ 0x12, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49,
+ 0x6e, 0x66, 0x6f, 0x12, 0x4a, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61,
+ 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32,
+ 0x1b, 0x2e, 0x61, 0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f,
+ 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x11, 0x61, 0x75,
+ 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12,
+ 0x4c, 0x0a, 0x10, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64,
+ 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x75, 0x64, 0x69,
+ 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61,
+ 0x64, 0x61, 0x74, 0x61, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0f, 0x72, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x36, 0x0a,
+ 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17,
+ 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
+ 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x48, 0x00, 0x52, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x88, 0x01, 0x01, 0x12, 0x4f, 0x0a, 0x11, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+ 0x65, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b,
+ 0x32, 0x1a, 0x2e, 0x61, 0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x42, 0x06, 0xba, 0x48,
+ 0x03, 0xc8, 0x01, 0x01, 0x52, 0x10, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4d, 0x65,
+ 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x38, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+ 0x73, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
+ 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63,
+ 0x74, 0x48, 0x01, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x88, 0x01, 0x01,
+ 0x12, 0x38, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0a, 0x20, 0x01,
+ 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
+ 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x48, 0x02, 0x52, 0x08, 0x6d,
+ 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x72,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f,
+ 0x6e, 0x73, 0x65, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
+ 0x22, 0xf3, 0x02, 0x0a, 0x12, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74,
+ 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x2d, 0x0a, 0x0c, 0x70, 0x72, 0x69, 0x6e, 0x63,
+ 0x69, 0x70, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba,
+ 0x48, 0x07, 0xc8, 0x01, 0x01, 0x72, 0x02, 0x10, 0x01, 0x52, 0x0b, 0x70, 0x72, 0x69, 0x6e, 0x63,
+ 0x69, 0x70, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x36, 0x0a, 0x0f, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69,
+ 0x70, 0x61, 0x6c, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42,
+ 0x0d, 0xba, 0x48, 0x0a, 0xc8, 0x01, 0x01, 0x72, 0x05, 0x10, 0x01, 0x18, 0xff, 0x01, 0x52, 0x0e,
+ 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x6e,
+ 0x0a, 0x14, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e,
+ 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x37, 0xba, 0x48,
+ 0x34, 0x72, 0x32, 0x32, 0x30, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x2d, 0x5d, 0x2b, 0x2f, 0x5b, 0x61,
+ 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x2d, 0x5d, 0x2b, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
+ 0x2d, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d,
+ 0x39, 0x2d, 0x5d, 0x2b, 0x24, 0x48, 0x00, 0x52, 0x12, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
+ 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x6d,
+ 0x0a, 0x1f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e,
+ 0x74, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x66,
+ 0x6f, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x61, 0x75, 0x64, 0x69, 0x74, 0x2e,
+ 0x76, 0x31, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e,
+ 0x74, 0x44, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x52,
+ 0x1c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x44,
+ 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x17, 0x0a,
+ 0x15, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e,
+ 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xf2, 0x01, 0x0a, 0x11, 0x41, 0x75, 0x74, 0x68, 0x6f,
+ 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x55, 0x0a, 0x08,
+ 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x39,
+ 0xba, 0x48, 0x36, 0xc8, 0x01, 0x01, 0x72, 0x31, 0x32, 0x2f, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x5d,
+ 0x2b, 0x2f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x2d, 0x5d, 0x2b, 0x28, 0x3f, 0x3a, 0x2f,
+ 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x2d, 0x5d, 0x2b, 0x2f, 0x5b, 0x61, 0x2d, 0x7a, 0x30,
+ 0x2d, 0x39, 0x2d, 0x5f, 0x5d, 0x2b, 0x29, 0x2a, 0x24, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75,
+ 0x72, 0x63, 0x65, 0x12, 0x4c, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f,
+ 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x27, 0xba, 0x48, 0x24, 0x72, 0x22, 0x32, 0x20,
+ 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x2d, 0x5d, 0x2b, 0x28, 0x3f, 0x3a, 0x5c, 0x2e, 0x5b, 0x61, 0x2d,
+ 0x7a, 0x2d, 0x5d, 0x2b, 0x29, 0x2a, 0x5c, 0x2e, 0x5b, 0x61, 0x2d, 0x7a, 0x2d, 0x5d, 0x2b, 0x24,
+ 0x48, 0x00, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x88, 0x01,
+ 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01,
+ 0x28, 0x08, 0x48, 0x01, 0x52, 0x07, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, 0x88, 0x01, 0x01,
+ 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x42,
+ 0x0a, 0x0a, 0x08, 0x5f, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, 0x22, 0x89, 0x0b, 0x0a, 0x10,
+ 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74,
+ 0x1a, 0xa8, 0x01, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, 0x49, 0x0a, 0x09, 0x70, 0x72, 0x69,
+ 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x2b, 0xba, 0x48,
+ 0x28, 0xc8, 0x01, 0x01, 0x72, 0x23, 0x32, 0x21, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a,
+ 0x30, 0x2d, 0x39, 0x2d, 0x25, 0x2e, 0x5d, 0x2b, 0x2f, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a,
+ 0x30, 0x2d, 0x39, 0x2d, 0x25, 0x2e, 0x5d, 0x2b, 0x24, 0x52, 0x09, 0x70, 0x72, 0x69, 0x6e, 0x63,
+ 0x69, 0x70, 0x61, 0x6c, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65,
+ 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63,
+ 0x65, 0x73, 0x12, 0x37, 0x0a, 0x06, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01,
+ 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
+ 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x42, 0x06, 0xba, 0x48, 0x03,
+ 0xc8, 0x01, 0x01, 0x52, 0x06, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x1a, 0xae, 0x04, 0x0a, 0x07,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x13, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x88, 0x01, 0x01, 0x12, 0x4a, 0x0a, 0x06,
+ 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x25, 0x2e, 0x61,
+ 0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74,
+ 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x2e, 0x48, 0x74, 0x74, 0x70, 0x4d, 0x65, 0x74,
+ 0x68, 0x6f, 0x64, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0x82, 0x01, 0x02, 0x10, 0x01,
+ 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x51, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64,
+ 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x61, 0x75, 0x64, 0x69,
+ 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x43, 0x6f,
+ 0x6e, 0x74, 0x65, 0x78, 0x74, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x48, 0x65,
+ 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8,
+ 0x01, 0x01, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x21, 0x0a, 0x04, 0x70,
+ 0x61, 0x74, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0d, 0xba, 0x48, 0x0a, 0xc8, 0x01,
+ 0x01, 0x72, 0x05, 0x10, 0x01, 0x18, 0xff, 0x01, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x1e,
+ 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48,
+ 0x07, 0xc8, 0x01, 0x01, 0x72, 0x02, 0x10, 0x01, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x22,
+ 0x0a, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a,
+ 0xba, 0x48, 0x07, 0xc8, 0x01, 0x01, 0x72, 0x02, 0x10, 0x01, 0x52, 0x06, 0x73, 0x63, 0x68, 0x65,
+ 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28,
+ 0x09, 0x48, 0x01, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x88, 0x01, 0x01, 0x12, 0x3b, 0x0a,
+ 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
+ 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69,
+ 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0xb2,
+ 0x01, 0x02, 0x38, 0x01, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x08, 0x70, 0x72,
+ 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48,
+ 0x07, 0xc8, 0x01, 0x01, 0x72, 0x02, 0x10, 0x01, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63,
+ 0x6f, 0x6c, 0x12, 0x3b, 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b,
+ 0x32, 0x1f, 0x2e, 0x61, 0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x74, 0x74, 0x72,
+ 0x69, 0x62, 0x75, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x2e, 0x41, 0x75, 0x74,
+ 0x68, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x04, 0x61, 0x75, 0x74, 0x68, 0x1a,
+ 0x3a, 0x0a, 0x0c, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12,
+ 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65,
+ 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x05, 0x0a, 0x03, 0x5f,
+ 0x69, 0x64, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x1a, 0x87, 0x03, 0x0a,
+ 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x12, 0x6e, 0x75, 0x6d,
+ 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c,
+ 0x75, 0x65, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x28, 0x00, 0x48, 0x00, 0x52, 0x10, 0x6e,
+ 0x75, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x88,
+ 0x01, 0x01, 0x12, 0x3d, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
+ 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
+ 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x07, 0xba,
+ 0x48, 0x04, 0x22, 0x02, 0x28, 0x00, 0x48, 0x01, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x88, 0x01,
+ 0x01, 0x12, 0x4a, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03,
+ 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x61, 0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x74,
+ 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x2e, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x45,
+ 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x3b, 0x0a,
+ 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
+ 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69,
+ 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0xb2,
+ 0x01, 0x02, 0x38, 0x01, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x1a, 0x3a, 0x0a, 0x0c, 0x48, 0x65,
+ 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,
+ 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05,
+ 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c,
+ 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x6e, 0x75, 0x6d, 0x5f, 0x72,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x42, 0x07, 0x0a,
+ 0x05, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x22, 0x8e, 0x02, 0x0a, 0x0a, 0x48, 0x74, 0x74, 0x70, 0x4d,
+ 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x1b, 0x0a, 0x17, 0x48, 0x54, 0x54, 0x50, 0x5f, 0x4d, 0x45,
+ 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44,
+ 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, 0x48, 0x54, 0x54, 0x50, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f,
+ 0x44, 0x5f, 0x4f, 0x54, 0x48, 0x45, 0x52, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x48, 0x54, 0x54,
+ 0x50, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x47, 0x45, 0x54, 0x10, 0x02, 0x12, 0x14,
+ 0x0a, 0x10, 0x48, 0x54, 0x54, 0x50, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x48, 0x45,
+ 0x41, 0x44, 0x10, 0x03, 0x12, 0x14, 0x0a, 0x10, 0x48, 0x54, 0x54, 0x50, 0x5f, 0x4d, 0x45, 0x54,
+ 0x48, 0x4f, 0x44, 0x5f, 0x50, 0x4f, 0x53, 0x54, 0x10, 0x04, 0x12, 0x13, 0x0a, 0x0f, 0x48, 0x54,
+ 0x54, 0x50, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x50, 0x55, 0x54, 0x10, 0x05, 0x12,
+ 0x16, 0x0a, 0x12, 0x48, 0x54, 0x54, 0x50, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x44,
+ 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x06, 0x12, 0x17, 0x0a, 0x13, 0x48, 0x54, 0x54, 0x50, 0x5f,
+ 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x10, 0x07,
+ 0x12, 0x17, 0x0a, 0x13, 0x48, 0x54, 0x54, 0x50, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f,
+ 0x4f, 0x50, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x10, 0x08, 0x12, 0x15, 0x0a, 0x11, 0x48, 0x54, 0x54,
+ 0x50, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x09,
+ 0x12, 0x15, 0x0a, 0x11, 0x48, 0x54, 0x54, 0x50, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f,
+ 0x50, 0x41, 0x54, 0x43, 0x48, 0x10, 0x0a, 0x22, 0xe1, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x27, 0x0a, 0x09, 0x63,
+ 0x61, 0x6c, 0x6c, 0x65, 0x72, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a,
+ 0xba, 0x48, 0x07, 0xc8, 0x01, 0x01, 0x72, 0x02, 0x70, 0x01, 0x52, 0x08, 0x63, 0x61, 0x6c, 0x6c,
+ 0x65, 0x72, 0x49, 0x70, 0x12, 0x4a, 0x0a, 0x1a, 0x63, 0x61, 0x6c, 0x6c, 0x65, 0x72, 0x5f, 0x73,
+ 0x75, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x61, 0x67, 0x65,
+ 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0d, 0xba, 0x48, 0x0a, 0xc8, 0x01, 0x01,
+ 0x72, 0x05, 0x10, 0x01, 0x18, 0xff, 0x01, 0x52, 0x17, 0x63, 0x61, 0x6c, 0x6c, 0x65, 0x72, 0x53,
+ 0x75, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x55, 0x73, 0x65, 0x72, 0x41, 0x67, 0x65, 0x6e, 0x74,
+ 0x12, 0x59, 0x0a, 0x12, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x61, 0x74, 0x74, 0x72,
+ 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x61,
+ 0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74,
+ 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+ 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x11, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x22, 0xb4, 0x02, 0x0a, 0x10,
+ 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
+ 0x12, 0x48, 0x0a, 0x0b, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c,
+ 0x75, 0x65, 0x42, 0x0a, 0xba, 0x48, 0x07, 0xc8, 0x01, 0x01, 0x1a, 0x02, 0x28, 0x00, 0x52, 0x0a,
+ 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x0d, 0x65, 0x72,
+ 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
+ 0x09, 0x48, 0x00, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
+ 0x65, 0x88, 0x01, 0x01, 0x12, 0x3c, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x64, 0x65,
+ 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f,
+ 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74,
+ 0x72, 0x75, 0x63, 0x74, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x44, 0x65, 0x74, 0x61, 0x69,
+ 0x6c, 0x73, 0x12, 0x5c, 0x0a, 0x13, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x61,
+ 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32,
+ 0x23, 0x2e, 0x61, 0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69,
+ 0x62, 0x75, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x2e, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x12, 0x72, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73,
+ 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61,
+ 0x67, 0x65, 0x22, 0xba, 0x04, 0x0a, 0x1c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63,
+ 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x44, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49,
+ 0x6e, 0x66, 0x6f, 0x12, 0x63, 0x0a, 0x10, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x70, 0x72,
+ 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x36, 0x2e,
+ 0x61, 0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
+ 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x44, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f,
+ 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x50, 0x72, 0x69, 0x6e,
+ 0x63, 0x69, 0x70, 0x61, 0x6c, 0x48, 0x00, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x50,
+ 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x12, 0x5a, 0x0a, 0x0d, 0x69, 0x64, 0x70, 0x5f,
+ 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
+ 0x33, 0x2e, 0x61, 0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69,
+ 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x44, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74,
+ 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x49, 0x64, 0x70, 0x50, 0x72, 0x69, 0x6e, 0x63,
+ 0x69, 0x70, 0x61, 0x6c, 0x48, 0x00, 0x52, 0x0c, 0x69, 0x64, 0x70, 0x50, 0x72, 0x69, 0x6e, 0x63,
+ 0x69, 0x70, 0x61, 0x6c, 0x1a, 0x6f, 0x0a, 0x0f, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x50, 0x72,
+ 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x12, 0x47, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x69,
+ 0x63, 0x65, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x48, 0x00, 0x52, 0x0f, 0x73, 0x65,
+ 0x72, 0x76, 0x69, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x88, 0x01, 0x01,
+ 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6d, 0x65, 0x74,
+ 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0xd3, 0x01, 0x0a, 0x0c, 0x49, 0x64, 0x70, 0x50, 0x72, 0x69,
+ 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x12, 0x2d, 0x0a, 0x0c, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69,
+ 0x70, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48,
+ 0x07, 0xc8, 0x01, 0x01, 0x72, 0x02, 0x10, 0x01, 0x52, 0x0b, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69,
+ 0x70, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x36, 0x0a, 0x0f, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70,
+ 0x61, 0x6c, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0d,
+ 0xba, 0x48, 0x0a, 0xc8, 0x01, 0x01, 0x72, 0x05, 0x10, 0x01, 0x18, 0xff, 0x01, 0x52, 0x0e, 0x70,
+ 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x47, 0x0a,
+ 0x10, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
+ 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
+ 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74,
+ 0x48, 0x00, 0x52, 0x0f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64,
+ 0x61, 0x74, 0x61, 0x88, 0x01, 0x01, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69,
+ 0x63, 0x65, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x42, 0x12, 0x0a, 0x09, 0x61,
+ 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x2a,
+ 0x96, 0x02, 0x0a, 0x0b, 0x4c, 0x6f, 0x67, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12,
+ 0x1c, 0x0a, 0x18, 0x4c, 0x4f, 0x47, 0x5f, 0x53, 0x45, 0x56, 0x45, 0x52, 0x49, 0x54, 0x59, 0x5f,
+ 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x18, 0x0a,
+ 0x14, 0x4c, 0x4f, 0x47, 0x5f, 0x53, 0x45, 0x56, 0x45, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x44, 0x45,
+ 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x64, 0x12, 0x17, 0x0a, 0x12, 0x4c, 0x4f, 0x47, 0x5f, 0x53,
+ 0x45, 0x56, 0x45, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0xc8, 0x01,
+ 0x12, 0x16, 0x0a, 0x11, 0x4c, 0x4f, 0x47, 0x5f, 0x53, 0x45, 0x56, 0x45, 0x52, 0x49, 0x54, 0x59,
+ 0x5f, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0xac, 0x02, 0x12, 0x18, 0x0a, 0x13, 0x4c, 0x4f, 0x47, 0x5f,
+ 0x53, 0x45, 0x56, 0x45, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x4e, 0x4f, 0x54, 0x49, 0x43, 0x45, 0x10,
+ 0x90, 0x03, 0x12, 0x19, 0x0a, 0x14, 0x4c, 0x4f, 0x47, 0x5f, 0x53, 0x45, 0x56, 0x45, 0x52, 0x49,
+ 0x54, 0x59, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0xf4, 0x03, 0x12, 0x17, 0x0a,
+ 0x12, 0x4c, 0x4f, 0x47, 0x5f, 0x53, 0x45, 0x56, 0x45, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x45, 0x52,
+ 0x52, 0x4f, 0x52, 0x10, 0xd8, 0x04, 0x12, 0x1a, 0x0a, 0x15, 0x4c, 0x4f, 0x47, 0x5f, 0x53, 0x45,
+ 0x56, 0x45, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c, 0x10,
+ 0xbc, 0x05, 0x12, 0x17, 0x0a, 0x12, 0x4c, 0x4f, 0x47, 0x5f, 0x53, 0x45, 0x56, 0x45, 0x52, 0x49,
+ 0x54, 0x59, 0x5f, 0x41, 0x4c, 0x45, 0x52, 0x54, 0x10, 0xa0, 0x06, 0x12, 0x1b, 0x0a, 0x16, 0x4c,
+ 0x4f, 0x47, 0x5f, 0x53, 0x45, 0x56, 0x45, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x45, 0x4d, 0x45, 0x52,
+ 0x47, 0x45, 0x4e, 0x43, 0x59, 0x10, 0x84, 0x07, 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_audit_event_proto_rawDescOnce sync.Once
+ file_audit_v1_audit_event_proto_rawDescData = file_audit_v1_audit_event_proto_rawDesc
+)
+
+func file_audit_v1_audit_event_proto_rawDescGZIP() []byte {
+ file_audit_v1_audit_event_proto_rawDescOnce.Do(func() {
+ file_audit_v1_audit_event_proto_rawDescData = protoimpl.X.CompressGZIP(file_audit_v1_audit_event_proto_rawDescData)
+ })
+ return file_audit_v1_audit_event_proto_rawDescData
+}
+
+var file_audit_v1_audit_event_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
+var file_audit_v1_audit_event_proto_msgTypes = make([]protoimpl.MessageInfo, 16)
+var file_audit_v1_audit_event_proto_goTypes = []any{
+ (LogSeverity)(0), // 0: audit.v1.LogSeverity
+ (AttributeContext_HttpMethod)(0), // 1: audit.v1.AttributeContext.HttpMethod
+ (*AuditLogEntry)(nil), // 2: audit.v1.AuditLogEntry
+ (*AuditLog)(nil), // 3: audit.v1.AuditLog
+ (*AuthenticationInfo)(nil), // 4: audit.v1.AuthenticationInfo
+ (*AuthorizationInfo)(nil), // 5: audit.v1.AuthorizationInfo
+ (*AttributeContext)(nil), // 6: audit.v1.AttributeContext
+ (*RequestMetadata)(nil), // 7: audit.v1.RequestMetadata
+ (*ResponseMetadata)(nil), // 8: audit.v1.ResponseMetadata
+ (*ServiceAccountDelegationInfo)(nil), // 9: audit.v1.ServiceAccountDelegationInfo
+ nil, // 10: audit.v1.AuditLogEntry.LabelsEntry
+ (*AttributeContext_Auth)(nil), // 11: audit.v1.AttributeContext.Auth
+ (*AttributeContext_Request)(nil), // 12: audit.v1.AttributeContext.Request
+ (*AttributeContext_Response)(nil), // 13: audit.v1.AttributeContext.Response
+ nil, // 14: audit.v1.AttributeContext.Request.HeadersEntry
+ nil, // 15: audit.v1.AttributeContext.Response.HeadersEntry
+ (*ServiceAccountDelegationInfo_SystemPrincipal)(nil), // 16: audit.v1.ServiceAccountDelegationInfo.SystemPrincipal
+ (*ServiceAccountDelegationInfo_IdpPrincipal)(nil), // 17: audit.v1.ServiceAccountDelegationInfo.IdpPrincipal
+ (*timestamppb.Timestamp)(nil), // 18: google.protobuf.Timestamp
+ (*structpb.Struct)(nil), // 19: google.protobuf.Struct
+ (*wrapperspb.Int32Value)(nil), // 20: google.protobuf.Int32Value
+ (*wrapperspb.Int64Value)(nil), // 21: google.protobuf.Int64Value
+}
+var file_audit_v1_audit_event_proto_depIdxs = []int32{
+ 3, // 0: audit.v1.AuditLogEntry.proto_payload:type_name -> audit.v1.AuditLog
+ 10, // 1: audit.v1.AuditLogEntry.labels:type_name -> audit.v1.AuditLogEntry.LabelsEntry
+ 18, // 2: audit.v1.AuditLogEntry.timestamp:type_name -> google.protobuf.Timestamp
+ 0, // 3: audit.v1.AuditLogEntry.severity:type_name -> audit.v1.LogSeverity
+ 4, // 4: audit.v1.AuditLog.authentication_info:type_name -> audit.v1.AuthenticationInfo
+ 5, // 5: audit.v1.AuditLog.authorization_info:type_name -> audit.v1.AuthorizationInfo
+ 7, // 6: audit.v1.AuditLog.request_metadata:type_name -> audit.v1.RequestMetadata
+ 19, // 7: audit.v1.AuditLog.request:type_name -> google.protobuf.Struct
+ 8, // 8: audit.v1.AuditLog.response_metadata:type_name -> audit.v1.ResponseMetadata
+ 19, // 9: audit.v1.AuditLog.response:type_name -> google.protobuf.Struct
+ 19, // 10: audit.v1.AuditLog.metadata:type_name -> google.protobuf.Struct
+ 9, // 11: audit.v1.AuthenticationInfo.service_account_delegation_info:type_name -> audit.v1.ServiceAccountDelegationInfo
+ 12, // 12: audit.v1.RequestMetadata.request_attributes:type_name -> audit.v1.AttributeContext.Request
+ 20, // 13: audit.v1.ResponseMetadata.status_code:type_name -> google.protobuf.Int32Value
+ 19, // 14: audit.v1.ResponseMetadata.error_details:type_name -> google.protobuf.Struct
+ 13, // 15: audit.v1.ResponseMetadata.response_attributes:type_name -> audit.v1.AttributeContext.Response
+ 16, // 16: audit.v1.ServiceAccountDelegationInfo.system_principal:type_name -> audit.v1.ServiceAccountDelegationInfo.SystemPrincipal
+ 17, // 17: audit.v1.ServiceAccountDelegationInfo.idp_principal:type_name -> audit.v1.ServiceAccountDelegationInfo.IdpPrincipal
+ 19, // 18: audit.v1.AttributeContext.Auth.claims:type_name -> google.protobuf.Struct
+ 1, // 19: audit.v1.AttributeContext.Request.method:type_name -> audit.v1.AttributeContext.HttpMethod
+ 14, // 20: audit.v1.AttributeContext.Request.headers:type_name -> audit.v1.AttributeContext.Request.HeadersEntry
+ 18, // 21: audit.v1.AttributeContext.Request.time:type_name -> google.protobuf.Timestamp
+ 11, // 22: audit.v1.AttributeContext.Request.auth:type_name -> audit.v1.AttributeContext.Auth
+ 21, // 23: audit.v1.AttributeContext.Response.num_response_items:type_name -> google.protobuf.Int64Value
+ 21, // 24: audit.v1.AttributeContext.Response.size:type_name -> google.protobuf.Int64Value
+ 15, // 25: audit.v1.AttributeContext.Response.headers:type_name -> audit.v1.AttributeContext.Response.HeadersEntry
+ 18, // 26: audit.v1.AttributeContext.Response.time:type_name -> google.protobuf.Timestamp
+ 19, // 27: audit.v1.ServiceAccountDelegationInfo.SystemPrincipal.service_metadata:type_name -> google.protobuf.Struct
+ 19, // 28: audit.v1.ServiceAccountDelegationInfo.IdpPrincipal.service_metadata:type_name -> google.protobuf.Struct
+ 29, // [29:29] is the sub-list for method output_type
+ 29, // [29:29] is the sub-list for method input_type
+ 29, // [29:29] is the sub-list for extension type_name
+ 29, // [29:29] is the sub-list for extension extendee
+ 0, // [0:29] is the sub-list for field type_name
+}
+
+func init() { file_audit_v1_audit_event_proto_init() }
+func file_audit_v1_audit_event_proto_init() {
+ if File_audit_v1_audit_event_proto != nil {
+ return
+ }
+ file_audit_v1_audit_event_proto_msgTypes[0].OneofWrappers = []any{}
+ file_audit_v1_audit_event_proto_msgTypes[1].OneofWrappers = []any{}
+ file_audit_v1_audit_event_proto_msgTypes[2].OneofWrappers = []any{}
+ file_audit_v1_audit_event_proto_msgTypes[3].OneofWrappers = []any{}
+ file_audit_v1_audit_event_proto_msgTypes[6].OneofWrappers = []any{}
+ file_audit_v1_audit_event_proto_msgTypes[7].OneofWrappers = []any{
+ (*ServiceAccountDelegationInfo_SystemPrincipal_)(nil),
+ (*ServiceAccountDelegationInfo_IdpPrincipal_)(nil),
+ }
+ file_audit_v1_audit_event_proto_msgTypes[10].OneofWrappers = []any{}
+ file_audit_v1_audit_event_proto_msgTypes[11].OneofWrappers = []any{}
+ file_audit_v1_audit_event_proto_msgTypes[14].OneofWrappers = []any{}
+ file_audit_v1_audit_event_proto_msgTypes[15].OneofWrappers = []any{}
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_audit_v1_audit_event_proto_rawDesc,
+ NumEnums: 2,
+ NumMessages: 16,
+ NumExtensions: 0,
+ NumServices: 0,
+ },
+ GoTypes: file_audit_v1_audit_event_proto_goTypes,
+ DependencyIndexes: file_audit_v1_audit_event_proto_depIdxs,
+ EnumInfos: file_audit_v1_audit_event_proto_enumTypes,
+ MessageInfos: file_audit_v1_audit_event_proto_msgTypes,
+ }.Build()
+ File_audit_v1_audit_event_proto = out.File
+ file_audit_v1_audit_event_proto_rawDesc = nil
+ file_audit_v1_audit_event_proto_goTypes = nil
+ file_audit_v1_audit_event_proto_depIdxs = nil
+}
diff --git a/gen/go/audit/v1/audit_event.pb.validate.go b/gen/go/audit/v1/audit_event.pb.validate.go
new file mode 100644
index 0000000..e6bf8cc
--- /dev/null
+++ b/gen/go/audit/v1/audit_event.pb.validate.go
@@ -0,0 +1,2209 @@
+// Code generated by protoc-gen-validate. DO NOT EDIT.
+// source: audit/v1/audit_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 AuditLogEntry 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 *AuditLogEntry) Validate() error {
+ return m.validate(false)
+}
+
+// ValidateAll checks the field values on AuditLogEntry 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 AuditLogEntryMultiError, or
+// nil if none found.
+func (m *AuditLogEntry) ValidateAll() error {
+ return m.validate(true)
+}
+
+func (m *AuditLogEntry) validate(all bool) error {
+ if m == nil {
+ return nil
+ }
+
+ var errors []error
+
+ // no validation rules for LogName
+
+ if all {
+ switch v := interface{}(m.GetProtoPayload()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, AuditLogEntryValidationError{
+ field: "ProtoPayload",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, AuditLogEntryValidationError{
+ field: "ProtoPayload",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetProtoPayload()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return AuditLogEntryValidationError{
+ field: "ProtoPayload",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ // no validation rules for InsertId
+
+ // no validation rules for Labels
+
+ if all {
+ switch v := interface{}(m.GetTimestamp()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, AuditLogEntryValidationError{
+ field: "Timestamp",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, AuditLogEntryValidationError{
+ field: "Timestamp",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetTimestamp()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return AuditLogEntryValidationError{
+ field: "Timestamp",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ // no validation rules for Severity
+
+ if m.CorrelationId != nil {
+ // no validation rules for CorrelationId
+ }
+
+ if m.TraceParent != nil {
+ // no validation rules for TraceParent
+ }
+
+ if m.TraceState != nil {
+ // no validation rules for TraceState
+ }
+
+ if len(errors) > 0 {
+ return AuditLogEntryMultiError(errors)
+ }
+
+ return nil
+}
+
+// AuditLogEntryMultiError is an error wrapping multiple validation errors
+// returned by AuditLogEntry.ValidateAll() if the designated constraints
+// aren't met.
+type AuditLogEntryMultiError []error
+
+// Error returns a concatenation of all the error messages it wraps.
+func (m AuditLogEntryMultiError) 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 AuditLogEntryMultiError) AllErrors() []error { return m }
+
+// AuditLogEntryValidationError is the validation error returned by
+// AuditLogEntry.Validate if the designated constraints aren't met.
+type AuditLogEntryValidationError struct {
+ field string
+ reason string
+ cause error
+ key bool
+}
+
+// Field function returns field value.
+func (e AuditLogEntryValidationError) Field() string { return e.field }
+
+// Reason function returns reason value.
+func (e AuditLogEntryValidationError) Reason() string { return e.reason }
+
+// Cause function returns cause value.
+func (e AuditLogEntryValidationError) Cause() error { return e.cause }
+
+// Key function returns key value.
+func (e AuditLogEntryValidationError) Key() bool { return e.key }
+
+// ErrorName returns error name.
+func (e AuditLogEntryValidationError) ErrorName() string { return "AuditLogEntryValidationError" }
+
+// Error satisfies the builtin error interface
+func (e AuditLogEntryValidationError) 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 %sAuditLogEntry.%s: %s%s",
+ key,
+ e.field,
+ e.reason,
+ cause)
+}
+
+var _ error = AuditLogEntryValidationError{}
+
+var _ interface {
+ Field() string
+ Reason() string
+ Key() bool
+ Cause() error
+ ErrorName() string
+} = AuditLogEntryValidationError{}
+
+// Validate checks the field values on AuditLog 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 *AuditLog) Validate() error {
+ return m.validate(false)
+}
+
+// ValidateAll checks the field values on AuditLog 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 AuditLogMultiError, or nil
+// if none found.
+func (m *AuditLog) ValidateAll() error {
+ return m.validate(true)
+}
+
+func (m *AuditLog) validate(all bool) error {
+ if m == nil {
+ return nil
+ }
+
+ var errors []error
+
+ // no validation rules for ServiceName
+
+ // no validation rules for OperationName
+
+ // no validation rules for ResourceName
+
+ if all {
+ switch v := interface{}(m.GetAuthenticationInfo()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, AuditLogValidationError{
+ field: "AuthenticationInfo",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, AuditLogValidationError{
+ field: "AuthenticationInfo",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetAuthenticationInfo()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return AuditLogValidationError{
+ field: "AuthenticationInfo",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ for idx, item := range m.GetAuthorizationInfo() {
+ _, _ = idx, item
+
+ if all {
+ switch v := interface{}(item).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, AuditLogValidationError{
+ field: fmt.Sprintf("AuthorizationInfo[%v]", idx),
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, AuditLogValidationError{
+ field: fmt.Sprintf("AuthorizationInfo[%v]", idx),
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(item).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return AuditLogValidationError{
+ field: fmt.Sprintf("AuthorizationInfo[%v]", idx),
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ }
+
+ if all {
+ switch v := interface{}(m.GetRequestMetadata()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, AuditLogValidationError{
+ field: "RequestMetadata",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, AuditLogValidationError{
+ field: "RequestMetadata",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetRequestMetadata()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return AuditLogValidationError{
+ field: "RequestMetadata",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ if all {
+ switch v := interface{}(m.GetResponseMetadata()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, AuditLogValidationError{
+ field: "ResponseMetadata",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, AuditLogValidationError{
+ field: "ResponseMetadata",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetResponseMetadata()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return AuditLogValidationError{
+ field: "ResponseMetadata",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ if m.Request != nil {
+
+ if all {
+ switch v := interface{}(m.GetRequest()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, AuditLogValidationError{
+ field: "Request",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, AuditLogValidationError{
+ field: "Request",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetRequest()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return AuditLogValidationError{
+ field: "Request",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ }
+
+ if m.Response != nil {
+
+ if all {
+ switch v := interface{}(m.GetResponse()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, AuditLogValidationError{
+ field: "Response",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, AuditLogValidationError{
+ field: "Response",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetResponse()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return AuditLogValidationError{
+ field: "Response",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ }
+
+ if m.Metadata != nil {
+
+ if all {
+ switch v := interface{}(m.GetMetadata()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, AuditLogValidationError{
+ field: "Metadata",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, AuditLogValidationError{
+ field: "Metadata",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetMetadata()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return AuditLogValidationError{
+ field: "Metadata",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ }
+
+ if len(errors) > 0 {
+ return AuditLogMultiError(errors)
+ }
+
+ return nil
+}
+
+// AuditLogMultiError is an error wrapping multiple validation errors returned
+// by AuditLog.ValidateAll() if the designated constraints aren't met.
+type AuditLogMultiError []error
+
+// Error returns a concatenation of all the error messages it wraps.
+func (m AuditLogMultiError) 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 AuditLogMultiError) AllErrors() []error { return m }
+
+// AuditLogValidationError is the validation error returned by
+// AuditLog.Validate if the designated constraints aren't met.
+type AuditLogValidationError struct {
+ field string
+ reason string
+ cause error
+ key bool
+}
+
+// Field function returns field value.
+func (e AuditLogValidationError) Field() string { return e.field }
+
+// Reason function returns reason value.
+func (e AuditLogValidationError) Reason() string { return e.reason }
+
+// Cause function returns cause value.
+func (e AuditLogValidationError) Cause() error { return e.cause }
+
+// Key function returns key value.
+func (e AuditLogValidationError) Key() bool { return e.key }
+
+// ErrorName returns error name.
+func (e AuditLogValidationError) ErrorName() string { return "AuditLogValidationError" }
+
+// Error satisfies the builtin error interface
+func (e AuditLogValidationError) 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 %sAuditLog.%s: %s%s",
+ key,
+ e.field,
+ e.reason,
+ cause)
+}
+
+var _ error = AuditLogValidationError{}
+
+var _ interface {
+ Field() string
+ Reason() string
+ Key() bool
+ Cause() error
+ ErrorName() string
+} = AuditLogValidationError{}
+
+// Validate checks the field values on AuthenticationInfo 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 *AuthenticationInfo) Validate() error {
+ return m.validate(false)
+}
+
+// ValidateAll checks the field values on AuthenticationInfo 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
+// AuthenticationInfoMultiError, or nil if none found.
+func (m *AuthenticationInfo) ValidateAll() error {
+ return m.validate(true)
+}
+
+func (m *AuthenticationInfo) validate(all bool) error {
+ if m == nil {
+ return nil
+ }
+
+ var errors []error
+
+ // no validation rules for PrincipalId
+
+ // no validation rules for PrincipalEmail
+
+ for idx, item := range m.GetServiceAccountDelegationInfo() {
+ _, _ = idx, item
+
+ if all {
+ switch v := interface{}(item).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, AuthenticationInfoValidationError{
+ field: fmt.Sprintf("ServiceAccountDelegationInfo[%v]", idx),
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, AuthenticationInfoValidationError{
+ field: fmt.Sprintf("ServiceAccountDelegationInfo[%v]", idx),
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(item).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return AuthenticationInfoValidationError{
+ field: fmt.Sprintf("ServiceAccountDelegationInfo[%v]", idx),
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ }
+
+ if m.ServiceAccountName != nil {
+ // no validation rules for ServiceAccountName
+ }
+
+ if len(errors) > 0 {
+ return AuthenticationInfoMultiError(errors)
+ }
+
+ return nil
+}
+
+// AuthenticationInfoMultiError is an error wrapping multiple validation errors
+// returned by AuthenticationInfo.ValidateAll() if the designated constraints
+// aren't met.
+type AuthenticationInfoMultiError []error
+
+// Error returns a concatenation of all the error messages it wraps.
+func (m AuthenticationInfoMultiError) 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 AuthenticationInfoMultiError) AllErrors() []error { return m }
+
+// AuthenticationInfoValidationError is the validation error returned by
+// AuthenticationInfo.Validate if the designated constraints aren't met.
+type AuthenticationInfoValidationError struct {
+ field string
+ reason string
+ cause error
+ key bool
+}
+
+// Field function returns field value.
+func (e AuthenticationInfoValidationError) Field() string { return e.field }
+
+// Reason function returns reason value.
+func (e AuthenticationInfoValidationError) Reason() string { return e.reason }
+
+// Cause function returns cause value.
+func (e AuthenticationInfoValidationError) Cause() error { return e.cause }
+
+// Key function returns key value.
+func (e AuthenticationInfoValidationError) Key() bool { return e.key }
+
+// ErrorName returns error name.
+func (e AuthenticationInfoValidationError) ErrorName() string {
+ return "AuthenticationInfoValidationError"
+}
+
+// Error satisfies the builtin error interface
+func (e AuthenticationInfoValidationError) 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 %sAuthenticationInfo.%s: %s%s",
+ key,
+ e.field,
+ e.reason,
+ cause)
+}
+
+var _ error = AuthenticationInfoValidationError{}
+
+var _ interface {
+ Field() string
+ Reason() string
+ Key() bool
+ Cause() error
+ ErrorName() string
+} = AuthenticationInfoValidationError{}
+
+// Validate checks the field values on AuthorizationInfo 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 *AuthorizationInfo) Validate() error {
+ return m.validate(false)
+}
+
+// ValidateAll checks the field values on AuthorizationInfo 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
+// AuthorizationInfoMultiError, or nil if none found.
+func (m *AuthorizationInfo) ValidateAll() error {
+ return m.validate(true)
+}
+
+func (m *AuthorizationInfo) validate(all bool) error {
+ if m == nil {
+ return nil
+ }
+
+ var errors []error
+
+ // no validation rules for Resource
+
+ if m.Permission != nil {
+ // no validation rules for Permission
+ }
+
+ if m.Granted != nil {
+ // no validation rules for Granted
+ }
+
+ if len(errors) > 0 {
+ return AuthorizationInfoMultiError(errors)
+ }
+
+ return nil
+}
+
+// AuthorizationInfoMultiError is an error wrapping multiple validation errors
+// returned by AuthorizationInfo.ValidateAll() if the designated constraints
+// aren't met.
+type AuthorizationInfoMultiError []error
+
+// Error returns a concatenation of all the error messages it wraps.
+func (m AuthorizationInfoMultiError) 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 AuthorizationInfoMultiError) AllErrors() []error { return m }
+
+// AuthorizationInfoValidationError is the validation error returned by
+// AuthorizationInfo.Validate if the designated constraints aren't met.
+type AuthorizationInfoValidationError struct {
+ field string
+ reason string
+ cause error
+ key bool
+}
+
+// Field function returns field value.
+func (e AuthorizationInfoValidationError) Field() string { return e.field }
+
+// Reason function returns reason value.
+func (e AuthorizationInfoValidationError) Reason() string { return e.reason }
+
+// Cause function returns cause value.
+func (e AuthorizationInfoValidationError) Cause() error { return e.cause }
+
+// Key function returns key value.
+func (e AuthorizationInfoValidationError) Key() bool { return e.key }
+
+// ErrorName returns error name.
+func (e AuthorizationInfoValidationError) ErrorName() string {
+ return "AuthorizationInfoValidationError"
+}
+
+// Error satisfies the builtin error interface
+func (e AuthorizationInfoValidationError) 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 %sAuthorizationInfo.%s: %s%s",
+ key,
+ e.field,
+ e.reason,
+ cause)
+}
+
+var _ error = AuthorizationInfoValidationError{}
+
+var _ interface {
+ Field() string
+ Reason() string
+ Key() bool
+ Cause() error
+ ErrorName() string
+} = AuthorizationInfoValidationError{}
+
+// Validate checks the field values on AttributeContext 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 *AttributeContext) Validate() error {
+ return m.validate(false)
+}
+
+// ValidateAll checks the field values on AttributeContext 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
+// AttributeContextMultiError, or nil if none found.
+func (m *AttributeContext) ValidateAll() error {
+ return m.validate(true)
+}
+
+func (m *AttributeContext) validate(all bool) error {
+ if m == nil {
+ return nil
+ }
+
+ var errors []error
+
+ if len(errors) > 0 {
+ return AttributeContextMultiError(errors)
+ }
+
+ return nil
+}
+
+// AttributeContextMultiError is an error wrapping multiple validation errors
+// returned by AttributeContext.ValidateAll() if the designated constraints
+// aren't met.
+type AttributeContextMultiError []error
+
+// Error returns a concatenation of all the error messages it wraps.
+func (m AttributeContextMultiError) 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 AttributeContextMultiError) AllErrors() []error { return m }
+
+// AttributeContextValidationError is the validation error returned by
+// AttributeContext.Validate if the designated constraints aren't met.
+type AttributeContextValidationError struct {
+ field string
+ reason string
+ cause error
+ key bool
+}
+
+// Field function returns field value.
+func (e AttributeContextValidationError) Field() string { return e.field }
+
+// Reason function returns reason value.
+func (e AttributeContextValidationError) Reason() string { return e.reason }
+
+// Cause function returns cause value.
+func (e AttributeContextValidationError) Cause() error { return e.cause }
+
+// Key function returns key value.
+func (e AttributeContextValidationError) Key() bool { return e.key }
+
+// ErrorName returns error name.
+func (e AttributeContextValidationError) ErrorName() string { return "AttributeContextValidationError" }
+
+// Error satisfies the builtin error interface
+func (e AttributeContextValidationError) 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 %sAttributeContext.%s: %s%s",
+ key,
+ e.field,
+ e.reason,
+ cause)
+}
+
+var _ error = AttributeContextValidationError{}
+
+var _ interface {
+ Field() string
+ Reason() string
+ Key() bool
+ Cause() error
+ ErrorName() string
+} = AttributeContextValidationError{}
+
+// Validate checks the field values on RequestMetadata 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 *RequestMetadata) Validate() error {
+ return m.validate(false)
+}
+
+// ValidateAll checks the field values on RequestMetadata 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
+// RequestMetadataMultiError, or nil if none found.
+func (m *RequestMetadata) ValidateAll() error {
+ return m.validate(true)
+}
+
+func (m *RequestMetadata) validate(all bool) error {
+ if m == nil {
+ return nil
+ }
+
+ var errors []error
+
+ // no validation rules for CallerIp
+
+ // no validation rules for CallerSuppliedUserAgent
+
+ if all {
+ switch v := interface{}(m.GetRequestAttributes()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, RequestMetadataValidationError{
+ field: "RequestAttributes",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, RequestMetadataValidationError{
+ field: "RequestAttributes",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetRequestAttributes()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return RequestMetadataValidationError{
+ field: "RequestAttributes",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ if len(errors) > 0 {
+ return RequestMetadataMultiError(errors)
+ }
+
+ return nil
+}
+
+// RequestMetadataMultiError is an error wrapping multiple validation errors
+// returned by RequestMetadata.ValidateAll() if the designated constraints
+// aren't met.
+type RequestMetadataMultiError []error
+
+// Error returns a concatenation of all the error messages it wraps.
+func (m RequestMetadataMultiError) 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 RequestMetadataMultiError) AllErrors() []error { return m }
+
+// RequestMetadataValidationError is the validation error returned by
+// RequestMetadata.Validate if the designated constraints aren't met.
+type RequestMetadataValidationError struct {
+ field string
+ reason string
+ cause error
+ key bool
+}
+
+// Field function returns field value.
+func (e RequestMetadataValidationError) Field() string { return e.field }
+
+// Reason function returns reason value.
+func (e RequestMetadataValidationError) Reason() string { return e.reason }
+
+// Cause function returns cause value.
+func (e RequestMetadataValidationError) Cause() error { return e.cause }
+
+// Key function returns key value.
+func (e RequestMetadataValidationError) Key() bool { return e.key }
+
+// ErrorName returns error name.
+func (e RequestMetadataValidationError) ErrorName() string { return "RequestMetadataValidationError" }
+
+// Error satisfies the builtin error interface
+func (e RequestMetadataValidationError) 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 %sRequestMetadata.%s: %s%s",
+ key,
+ e.field,
+ e.reason,
+ cause)
+}
+
+var _ error = RequestMetadataValidationError{}
+
+var _ interface {
+ Field() string
+ Reason() string
+ Key() bool
+ Cause() error
+ ErrorName() string
+} = RequestMetadataValidationError{}
+
+// Validate checks the field values on ResponseMetadata 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 *ResponseMetadata) Validate() error {
+ return m.validate(false)
+}
+
+// ValidateAll checks the field values on ResponseMetadata 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
+// ResponseMetadataMultiError, or nil if none found.
+func (m *ResponseMetadata) ValidateAll() error {
+ return m.validate(true)
+}
+
+func (m *ResponseMetadata) validate(all bool) error {
+ if m == nil {
+ return nil
+ }
+
+ var errors []error
+
+ if all {
+ switch v := interface{}(m.GetStatusCode()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, ResponseMetadataValidationError{
+ field: "StatusCode",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, ResponseMetadataValidationError{
+ field: "StatusCode",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetStatusCode()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return ResponseMetadataValidationError{
+ field: "StatusCode",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ for idx, item := range m.GetErrorDetails() {
+ _, _ = idx, item
+
+ if all {
+ switch v := interface{}(item).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, ResponseMetadataValidationError{
+ field: fmt.Sprintf("ErrorDetails[%v]", idx),
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, ResponseMetadataValidationError{
+ field: fmt.Sprintf("ErrorDetails[%v]", idx),
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(item).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return ResponseMetadataValidationError{
+ field: fmt.Sprintf("ErrorDetails[%v]", idx),
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ }
+
+ if all {
+ switch v := interface{}(m.GetResponseAttributes()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, ResponseMetadataValidationError{
+ field: "ResponseAttributes",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, ResponseMetadataValidationError{
+ field: "ResponseAttributes",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetResponseAttributes()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return ResponseMetadataValidationError{
+ field: "ResponseAttributes",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ if m.ErrorMessage != nil {
+ // no validation rules for ErrorMessage
+ }
+
+ if len(errors) > 0 {
+ return ResponseMetadataMultiError(errors)
+ }
+
+ return nil
+}
+
+// ResponseMetadataMultiError is an error wrapping multiple validation errors
+// returned by ResponseMetadata.ValidateAll() if the designated constraints
+// aren't met.
+type ResponseMetadataMultiError []error
+
+// Error returns a concatenation of all the error messages it wraps.
+func (m ResponseMetadataMultiError) 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 ResponseMetadataMultiError) AllErrors() []error { return m }
+
+// ResponseMetadataValidationError is the validation error returned by
+// ResponseMetadata.Validate if the designated constraints aren't met.
+type ResponseMetadataValidationError struct {
+ field string
+ reason string
+ cause error
+ key bool
+}
+
+// Field function returns field value.
+func (e ResponseMetadataValidationError) Field() string { return e.field }
+
+// Reason function returns reason value.
+func (e ResponseMetadataValidationError) Reason() string { return e.reason }
+
+// Cause function returns cause value.
+func (e ResponseMetadataValidationError) Cause() error { return e.cause }
+
+// Key function returns key value.
+func (e ResponseMetadataValidationError) Key() bool { return e.key }
+
+// ErrorName returns error name.
+func (e ResponseMetadataValidationError) ErrorName() string { return "ResponseMetadataValidationError" }
+
+// Error satisfies the builtin error interface
+func (e ResponseMetadataValidationError) 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 %sResponseMetadata.%s: %s%s",
+ key,
+ e.field,
+ e.reason,
+ cause)
+}
+
+var _ error = ResponseMetadataValidationError{}
+
+var _ interface {
+ Field() string
+ Reason() string
+ Key() bool
+ Cause() error
+ ErrorName() string
+} = ResponseMetadataValidationError{}
+
+// Validate checks the field values on ServiceAccountDelegationInfo 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 *ServiceAccountDelegationInfo) Validate() error {
+ return m.validate(false)
+}
+
+// ValidateAll checks the field values on ServiceAccountDelegationInfo 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
+// ServiceAccountDelegationInfoMultiError, or nil if none found.
+func (m *ServiceAccountDelegationInfo) ValidateAll() error {
+ return m.validate(true)
+}
+
+func (m *ServiceAccountDelegationInfo) validate(all bool) error {
+ if m == nil {
+ return nil
+ }
+
+ var errors []error
+
+ switch v := m.Authority.(type) {
+ case *ServiceAccountDelegationInfo_SystemPrincipal_:
+ if v == nil {
+ err := ServiceAccountDelegationInfoValidationError{
+ field: "Authority",
+ reason: "oneof value cannot be a typed-nil",
+ }
+ if !all {
+ return err
+ }
+ errors = append(errors, err)
+ }
+
+ if all {
+ switch v := interface{}(m.GetSystemPrincipal()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, ServiceAccountDelegationInfoValidationError{
+ field: "SystemPrincipal",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, ServiceAccountDelegationInfoValidationError{
+ field: "SystemPrincipal",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetSystemPrincipal()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return ServiceAccountDelegationInfoValidationError{
+ field: "SystemPrincipal",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ case *ServiceAccountDelegationInfo_IdpPrincipal_:
+ if v == nil {
+ err := ServiceAccountDelegationInfoValidationError{
+ field: "Authority",
+ reason: "oneof value cannot be a typed-nil",
+ }
+ if !all {
+ return err
+ }
+ errors = append(errors, err)
+ }
+
+ if all {
+ switch v := interface{}(m.GetIdpPrincipal()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, ServiceAccountDelegationInfoValidationError{
+ field: "IdpPrincipal",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, ServiceAccountDelegationInfoValidationError{
+ field: "IdpPrincipal",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetIdpPrincipal()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return ServiceAccountDelegationInfoValidationError{
+ field: "IdpPrincipal",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ default:
+ _ = v // ensures v is used
+ }
+
+ if len(errors) > 0 {
+ return ServiceAccountDelegationInfoMultiError(errors)
+ }
+
+ return nil
+}
+
+// ServiceAccountDelegationInfoMultiError is an error wrapping multiple
+// validation errors returned by ServiceAccountDelegationInfo.ValidateAll() if
+// the designated constraints aren't met.
+type ServiceAccountDelegationInfoMultiError []error
+
+// Error returns a concatenation of all the error messages it wraps.
+func (m ServiceAccountDelegationInfoMultiError) 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 ServiceAccountDelegationInfoMultiError) AllErrors() []error { return m }
+
+// ServiceAccountDelegationInfoValidationError is the validation error returned
+// by ServiceAccountDelegationInfo.Validate if the designated constraints
+// aren't met.
+type ServiceAccountDelegationInfoValidationError struct {
+ field string
+ reason string
+ cause error
+ key bool
+}
+
+// Field function returns field value.
+func (e ServiceAccountDelegationInfoValidationError) Field() string { return e.field }
+
+// Reason function returns reason value.
+func (e ServiceAccountDelegationInfoValidationError) Reason() string { return e.reason }
+
+// Cause function returns cause value.
+func (e ServiceAccountDelegationInfoValidationError) Cause() error { return e.cause }
+
+// Key function returns key value.
+func (e ServiceAccountDelegationInfoValidationError) Key() bool { return e.key }
+
+// ErrorName returns error name.
+func (e ServiceAccountDelegationInfoValidationError) ErrorName() string {
+ return "ServiceAccountDelegationInfoValidationError"
+}
+
+// Error satisfies the builtin error interface
+func (e ServiceAccountDelegationInfoValidationError) 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 %sServiceAccountDelegationInfo.%s: %s%s",
+ key,
+ e.field,
+ e.reason,
+ cause)
+}
+
+var _ error = ServiceAccountDelegationInfoValidationError{}
+
+var _ interface {
+ Field() string
+ Reason() string
+ Key() bool
+ Cause() error
+ ErrorName() string
+} = ServiceAccountDelegationInfoValidationError{}
+
+// Validate checks the field values on AttributeContext_Auth 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 *AttributeContext_Auth) Validate() error {
+ return m.validate(false)
+}
+
+// ValidateAll checks the field values on AttributeContext_Auth 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
+// AttributeContext_AuthMultiError, or nil if none found.
+func (m *AttributeContext_Auth) ValidateAll() error {
+ return m.validate(true)
+}
+
+func (m *AttributeContext_Auth) validate(all bool) error {
+ if m == nil {
+ return nil
+ }
+
+ var errors []error
+
+ // no validation rules for Principal
+
+ if all {
+ switch v := interface{}(m.GetClaims()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, AttributeContext_AuthValidationError{
+ field: "Claims",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, AttributeContext_AuthValidationError{
+ field: "Claims",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetClaims()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return AttributeContext_AuthValidationError{
+ field: "Claims",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ if len(errors) > 0 {
+ return AttributeContext_AuthMultiError(errors)
+ }
+
+ return nil
+}
+
+// AttributeContext_AuthMultiError is an error wrapping multiple validation
+// errors returned by AttributeContext_Auth.ValidateAll() if the designated
+// constraints aren't met.
+type AttributeContext_AuthMultiError []error
+
+// Error returns a concatenation of all the error messages it wraps.
+func (m AttributeContext_AuthMultiError) 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 AttributeContext_AuthMultiError) AllErrors() []error { return m }
+
+// AttributeContext_AuthValidationError is the validation error returned by
+// AttributeContext_Auth.Validate if the designated constraints aren't met.
+type AttributeContext_AuthValidationError struct {
+ field string
+ reason string
+ cause error
+ key bool
+}
+
+// Field function returns field value.
+func (e AttributeContext_AuthValidationError) Field() string { return e.field }
+
+// Reason function returns reason value.
+func (e AttributeContext_AuthValidationError) Reason() string { return e.reason }
+
+// Cause function returns cause value.
+func (e AttributeContext_AuthValidationError) Cause() error { return e.cause }
+
+// Key function returns key value.
+func (e AttributeContext_AuthValidationError) Key() bool { return e.key }
+
+// ErrorName returns error name.
+func (e AttributeContext_AuthValidationError) ErrorName() string {
+ return "AttributeContext_AuthValidationError"
+}
+
+// Error satisfies the builtin error interface
+func (e AttributeContext_AuthValidationError) 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 %sAttributeContext_Auth.%s: %s%s",
+ key,
+ e.field,
+ e.reason,
+ cause)
+}
+
+var _ error = AttributeContext_AuthValidationError{}
+
+var _ interface {
+ Field() string
+ Reason() string
+ Key() bool
+ Cause() error
+ ErrorName() string
+} = AttributeContext_AuthValidationError{}
+
+// Validate checks the field values on AttributeContext_Request 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 *AttributeContext_Request) Validate() error {
+ return m.validate(false)
+}
+
+// ValidateAll checks the field values on AttributeContext_Request 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
+// AttributeContext_RequestMultiError, or nil if none found.
+func (m *AttributeContext_Request) ValidateAll() error {
+ return m.validate(true)
+}
+
+func (m *AttributeContext_Request) validate(all bool) error {
+ if m == nil {
+ return nil
+ }
+
+ var errors []error
+
+ // no validation rules for Method
+
+ // no validation rules for Headers
+
+ // no validation rules for Path
+
+ // no validation rules for Host
+
+ // no validation rules for Scheme
+
+ if all {
+ switch v := interface{}(m.GetTime()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, AttributeContext_RequestValidationError{
+ field: "Time",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, AttributeContext_RequestValidationError{
+ field: "Time",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetTime()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return AttributeContext_RequestValidationError{
+ field: "Time",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ // no validation rules for Protocol
+
+ if all {
+ switch v := interface{}(m.GetAuth()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, AttributeContext_RequestValidationError{
+ field: "Auth",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, AttributeContext_RequestValidationError{
+ field: "Auth",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetAuth()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return AttributeContext_RequestValidationError{
+ field: "Auth",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ if m.Id != nil {
+ // no validation rules for Id
+ }
+
+ if m.Query != nil {
+ // no validation rules for Query
+ }
+
+ if len(errors) > 0 {
+ return AttributeContext_RequestMultiError(errors)
+ }
+
+ return nil
+}
+
+// AttributeContext_RequestMultiError is an error wrapping multiple validation
+// errors returned by AttributeContext_Request.ValidateAll() if the designated
+// constraints aren't met.
+type AttributeContext_RequestMultiError []error
+
+// Error returns a concatenation of all the error messages it wraps.
+func (m AttributeContext_RequestMultiError) 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 AttributeContext_RequestMultiError) AllErrors() []error { return m }
+
+// AttributeContext_RequestValidationError is the validation error returned by
+// AttributeContext_Request.Validate if the designated constraints aren't met.
+type AttributeContext_RequestValidationError struct {
+ field string
+ reason string
+ cause error
+ key bool
+}
+
+// Field function returns field value.
+func (e AttributeContext_RequestValidationError) Field() string { return e.field }
+
+// Reason function returns reason value.
+func (e AttributeContext_RequestValidationError) Reason() string { return e.reason }
+
+// Cause function returns cause value.
+func (e AttributeContext_RequestValidationError) Cause() error { return e.cause }
+
+// Key function returns key value.
+func (e AttributeContext_RequestValidationError) Key() bool { return e.key }
+
+// ErrorName returns error name.
+func (e AttributeContext_RequestValidationError) ErrorName() string {
+ return "AttributeContext_RequestValidationError"
+}
+
+// Error satisfies the builtin error interface
+func (e AttributeContext_RequestValidationError) 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 %sAttributeContext_Request.%s: %s%s",
+ key,
+ e.field,
+ e.reason,
+ cause)
+}
+
+var _ error = AttributeContext_RequestValidationError{}
+
+var _ interface {
+ Field() string
+ Reason() string
+ Key() bool
+ Cause() error
+ ErrorName() string
+} = AttributeContext_RequestValidationError{}
+
+// Validate checks the field values on AttributeContext_Response 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 *AttributeContext_Response) Validate() error {
+ return m.validate(false)
+}
+
+// ValidateAll checks the field values on AttributeContext_Response 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
+// AttributeContext_ResponseMultiError, or nil if none found.
+func (m *AttributeContext_Response) ValidateAll() error {
+ return m.validate(true)
+}
+
+func (m *AttributeContext_Response) validate(all bool) error {
+ if m == nil {
+ return nil
+ }
+
+ var errors []error
+
+ // no validation rules for Headers
+
+ if all {
+ switch v := interface{}(m.GetTime()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, AttributeContext_ResponseValidationError{
+ field: "Time",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, AttributeContext_ResponseValidationError{
+ field: "Time",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetTime()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return AttributeContext_ResponseValidationError{
+ field: "Time",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ if m.NumResponseItems != nil {
+
+ if all {
+ switch v := interface{}(m.GetNumResponseItems()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, AttributeContext_ResponseValidationError{
+ field: "NumResponseItems",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, AttributeContext_ResponseValidationError{
+ field: "NumResponseItems",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetNumResponseItems()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return AttributeContext_ResponseValidationError{
+ field: "NumResponseItems",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ }
+
+ if m.Size != nil {
+
+ if all {
+ switch v := interface{}(m.GetSize()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, AttributeContext_ResponseValidationError{
+ field: "Size",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, AttributeContext_ResponseValidationError{
+ field: "Size",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetSize()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return AttributeContext_ResponseValidationError{
+ field: "Size",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ }
+
+ if len(errors) > 0 {
+ return AttributeContext_ResponseMultiError(errors)
+ }
+
+ return nil
+}
+
+// AttributeContext_ResponseMultiError is an error wrapping multiple validation
+// errors returned by AttributeContext_Response.ValidateAll() if the
+// designated constraints aren't met.
+type AttributeContext_ResponseMultiError []error
+
+// Error returns a concatenation of all the error messages it wraps.
+func (m AttributeContext_ResponseMultiError) 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 AttributeContext_ResponseMultiError) AllErrors() []error { return m }
+
+// AttributeContext_ResponseValidationError is the validation error returned by
+// AttributeContext_Response.Validate if the designated constraints aren't met.
+type AttributeContext_ResponseValidationError struct {
+ field string
+ reason string
+ cause error
+ key bool
+}
+
+// Field function returns field value.
+func (e AttributeContext_ResponseValidationError) Field() string { return e.field }
+
+// Reason function returns reason value.
+func (e AttributeContext_ResponseValidationError) Reason() string { return e.reason }
+
+// Cause function returns cause value.
+func (e AttributeContext_ResponseValidationError) Cause() error { return e.cause }
+
+// Key function returns key value.
+func (e AttributeContext_ResponseValidationError) Key() bool { return e.key }
+
+// ErrorName returns error name.
+func (e AttributeContext_ResponseValidationError) ErrorName() string {
+ return "AttributeContext_ResponseValidationError"
+}
+
+// Error satisfies the builtin error interface
+func (e AttributeContext_ResponseValidationError) 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 %sAttributeContext_Response.%s: %s%s",
+ key,
+ e.field,
+ e.reason,
+ cause)
+}
+
+var _ error = AttributeContext_ResponseValidationError{}
+
+var _ interface {
+ Field() string
+ Reason() string
+ Key() bool
+ Cause() error
+ ErrorName() string
+} = AttributeContext_ResponseValidationError{}
+
+// Validate checks the field values on
+// ServiceAccountDelegationInfo_SystemPrincipal 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 *ServiceAccountDelegationInfo_SystemPrincipal) Validate() error {
+ return m.validate(false)
+}
+
+// ValidateAll checks the field values on
+// ServiceAccountDelegationInfo_SystemPrincipal 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
+// ServiceAccountDelegationInfo_SystemPrincipalMultiError, or nil if none found.
+func (m *ServiceAccountDelegationInfo_SystemPrincipal) ValidateAll() error {
+ return m.validate(true)
+}
+
+func (m *ServiceAccountDelegationInfo_SystemPrincipal) validate(all bool) error {
+ if m == nil {
+ return nil
+ }
+
+ var errors []error
+
+ if m.ServiceMetadata != nil {
+
+ if all {
+ switch v := interface{}(m.GetServiceMetadata()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, ServiceAccountDelegationInfo_SystemPrincipalValidationError{
+ field: "ServiceMetadata",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, ServiceAccountDelegationInfo_SystemPrincipalValidationError{
+ field: "ServiceMetadata",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetServiceMetadata()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return ServiceAccountDelegationInfo_SystemPrincipalValidationError{
+ field: "ServiceMetadata",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ }
+
+ if len(errors) > 0 {
+ return ServiceAccountDelegationInfo_SystemPrincipalMultiError(errors)
+ }
+
+ return nil
+}
+
+// ServiceAccountDelegationInfo_SystemPrincipalMultiError is an error wrapping
+// multiple validation errors returned by
+// ServiceAccountDelegationInfo_SystemPrincipal.ValidateAll() if the
+// designated constraints aren't met.
+type ServiceAccountDelegationInfo_SystemPrincipalMultiError []error
+
+// Error returns a concatenation of all the error messages it wraps.
+func (m ServiceAccountDelegationInfo_SystemPrincipalMultiError) 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 ServiceAccountDelegationInfo_SystemPrincipalMultiError) AllErrors() []error { return m }
+
+// ServiceAccountDelegationInfo_SystemPrincipalValidationError is the
+// validation error returned by
+// ServiceAccountDelegationInfo_SystemPrincipal.Validate if the designated
+// constraints aren't met.
+type ServiceAccountDelegationInfo_SystemPrincipalValidationError struct {
+ field string
+ reason string
+ cause error
+ key bool
+}
+
+// Field function returns field value.
+func (e ServiceAccountDelegationInfo_SystemPrincipalValidationError) Field() string { return e.field }
+
+// Reason function returns reason value.
+func (e ServiceAccountDelegationInfo_SystemPrincipalValidationError) Reason() string { return e.reason }
+
+// Cause function returns cause value.
+func (e ServiceAccountDelegationInfo_SystemPrincipalValidationError) Cause() error { return e.cause }
+
+// Key function returns key value.
+func (e ServiceAccountDelegationInfo_SystemPrincipalValidationError) Key() bool { return e.key }
+
+// ErrorName returns error name.
+func (e ServiceAccountDelegationInfo_SystemPrincipalValidationError) ErrorName() string {
+ return "ServiceAccountDelegationInfo_SystemPrincipalValidationError"
+}
+
+// Error satisfies the builtin error interface
+func (e ServiceAccountDelegationInfo_SystemPrincipalValidationError) 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 %sServiceAccountDelegationInfo_SystemPrincipal.%s: %s%s",
+ key,
+ e.field,
+ e.reason,
+ cause)
+}
+
+var _ error = ServiceAccountDelegationInfo_SystemPrincipalValidationError{}
+
+var _ interface {
+ Field() string
+ Reason() string
+ Key() bool
+ Cause() error
+ ErrorName() string
+} = ServiceAccountDelegationInfo_SystemPrincipalValidationError{}
+
+// Validate checks the field values on
+// ServiceAccountDelegationInfo_IdpPrincipal 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 *ServiceAccountDelegationInfo_IdpPrincipal) Validate() error {
+ return m.validate(false)
+}
+
+// ValidateAll checks the field values on
+// ServiceAccountDelegationInfo_IdpPrincipal 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
+// ServiceAccountDelegationInfo_IdpPrincipalMultiError, or nil if none found.
+func (m *ServiceAccountDelegationInfo_IdpPrincipal) ValidateAll() error {
+ return m.validate(true)
+}
+
+func (m *ServiceAccountDelegationInfo_IdpPrincipal) validate(all bool) error {
+ if m == nil {
+ return nil
+ }
+
+ var errors []error
+
+ // no validation rules for PrincipalId
+
+ // no validation rules for PrincipalEmail
+
+ if m.ServiceMetadata != nil {
+
+ if all {
+ switch v := interface{}(m.GetServiceMetadata()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, ServiceAccountDelegationInfo_IdpPrincipalValidationError{
+ field: "ServiceMetadata",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, ServiceAccountDelegationInfo_IdpPrincipalValidationError{
+ field: "ServiceMetadata",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetServiceMetadata()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return ServiceAccountDelegationInfo_IdpPrincipalValidationError{
+ field: "ServiceMetadata",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ }
+
+ if len(errors) > 0 {
+ return ServiceAccountDelegationInfo_IdpPrincipalMultiError(errors)
+ }
+
+ return nil
+}
+
+// ServiceAccountDelegationInfo_IdpPrincipalMultiError is an error wrapping
+// multiple validation errors returned by
+// ServiceAccountDelegationInfo_IdpPrincipal.ValidateAll() if the designated
+// constraints aren't met.
+type ServiceAccountDelegationInfo_IdpPrincipalMultiError []error
+
+// Error returns a concatenation of all the error messages it wraps.
+func (m ServiceAccountDelegationInfo_IdpPrincipalMultiError) 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 ServiceAccountDelegationInfo_IdpPrincipalMultiError) AllErrors() []error { return m }
+
+// ServiceAccountDelegationInfo_IdpPrincipalValidationError is the validation
+// error returned by ServiceAccountDelegationInfo_IdpPrincipal.Validate if the
+// designated constraints aren't met.
+type ServiceAccountDelegationInfo_IdpPrincipalValidationError struct {
+ field string
+ reason string
+ cause error
+ key bool
+}
+
+// Field function returns field value.
+func (e ServiceAccountDelegationInfo_IdpPrincipalValidationError) Field() string { return e.field }
+
+// Reason function returns reason value.
+func (e ServiceAccountDelegationInfo_IdpPrincipalValidationError) Reason() string { return e.reason }
+
+// Cause function returns cause value.
+func (e ServiceAccountDelegationInfo_IdpPrincipalValidationError) Cause() error { return e.cause }
+
+// Key function returns key value.
+func (e ServiceAccountDelegationInfo_IdpPrincipalValidationError) Key() bool { return e.key }
+
+// ErrorName returns error name.
+func (e ServiceAccountDelegationInfo_IdpPrincipalValidationError) ErrorName() string {
+ return "ServiceAccountDelegationInfo_IdpPrincipalValidationError"
+}
+
+// Error satisfies the builtin error interface
+func (e ServiceAccountDelegationInfo_IdpPrincipalValidationError) 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 %sServiceAccountDelegationInfo_IdpPrincipal.%s: %s%s",
+ key,
+ e.field,
+ e.reason,
+ cause)
+}
+
+var _ error = ServiceAccountDelegationInfo_IdpPrincipalValidationError{}
+
+var _ interface {
+ Field() string
+ Reason() string
+ Key() bool
+ Cause() error
+ ErrorName() string
+} = ServiceAccountDelegationInfo_IdpPrincipalValidationError{}
diff --git a/gen/go/audit/v1/routable_event.pb.go b/gen/go/audit/v1/routable_event.pb.go
new file mode 100644
index 0000000..dd4d7dc
--- /dev/null
+++ b/gen/go/audit/v1/routable_event.pb.go
@@ -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....
+ // 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
+}
diff --git a/gen/go/audit/v1/routable_event.pb.validate.go b/gen/go/audit/v1/routable_event.pb.validate.go
new file mode 100644
index 0000000..4eee7ee
--- /dev/null
+++ b/gen/go/audit/v1/routable_event.pb.validate.go
@@ -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{}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..b3d6dd8
--- /dev/null
+++ b/go.mod
@@ -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
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..c451dc1
--- /dev/null
+++ b/go.sum
@@ -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=
diff --git a/log/log.go b/log/log.go
new file mode 100644
index 0000000..af5da7b
--- /dev/null
+++ b/log/log.go
@@ -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
+}
diff --git a/log/log_test.go b/log/log_test.go
new file mode 100644
index 0000000..e7f0d3e
--- /dev/null
+++ b/log/log_test.go
@@ -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"))
+ })
+}
diff --git a/log/slog.go b/log/slog.go
new file mode 100644
index 0000000..882456a
--- /dev/null
+++ b/log/slog.go
@@ -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
+}
diff --git a/log/slog_test.go b/log/slog_test.go
new file mode 100644
index 0000000..122bc80
--- /dev/null
+++ b/log/slog_test.go
@@ -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"))
+ })
+}
diff --git a/log/zerolog.go b/log/zerolog.go
new file mode 100644
index 0000000..ea4a634
--- /dev/null
+++ b/log/zerolog.go
@@ -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)
+}
diff --git a/log/zerolog_test.go b/log/zerolog_test.go
new file mode 100644
index 0000000..3f07d42
--- /dev/null
+++ b/log/zerolog_test.go
@@ -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"))
+ })
+}
diff --git a/proto/audit/v1/audit_event.proto b/proto/audit/v1/audit_event.proto
new file mode 100644
index 0000000..208df5a
--- /dev/null
+++ b/proto/audit/v1/audit_event.proto
@@ -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: //logs/
+ // 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: ///
+ // 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 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: ---
+ //
+ // 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: =[,=]
+ //
+ // 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....
+ // 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: /[/]
+ // Where:
+ // Plural-Type: One from the list of supported ObjectType as plural
+ // Id: The identifier of the object
+ // Details: Optional "/" 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//service-accounts/
+ //
+ // 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: /[/]
+ // Where:
+ // Plural-Type: One from the list of supported ObjectType as plural
+ // Id: The identifier of the object
+ // Details: Optional "/" 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: /
+ // 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": } 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:
+ // 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 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 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;
+ }
+}
diff --git a/proto/audit/v1/routable_event.proto b/proto/audit/v1/routable_event.proto
new file mode 100644
index 0000000..81016ff
--- /dev/null
+++ b/proto/audit/v1/routable_event.proto
@@ -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....
+ // 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;
+ }
+}
diff --git a/proto/buf.gen.yaml b/proto/buf.gen.yaml
new file mode 100644
index 0000000..0e1e54a
--- /dev/null
+++ b/proto/buf.gen.yaml
@@ -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
\ No newline at end of file
diff --git a/proto/buf.lock b/proto/buf.lock
new file mode 100644
index 0000000..d665f94
--- /dev/null
+++ b/proto/buf.lock
@@ -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
diff --git a/proto/buf.yaml b/proto/buf.yaml
new file mode 100644
index 0000000..028ea08
--- /dev/null
+++ b/proto/buf.yaml
@@ -0,0 +1,9 @@
+version: v1
+breaking:
+ use:
+ - FILE
+deps:
+ - buf.build/bufbuild/protovalidate
+lint:
+ use:
+ - STANDARD
\ No newline at end of file
diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go
new file mode 100644
index 0000000..921f42f
--- /dev/null
+++ b/telemetry/telemetry.go
@@ -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
+}