From 9337231a6fd85e5eeede7e6c86c549972cecdc0d Mon Sep 17 00:00:00 2001 From: Christian Schaible Date: Wed, 30 Oct 2024 10:32:07 +0000 Subject: [PATCH] Merged PR 666097: feat: Add implementation of core library Related work items: #687250 --- .azuredevops/build-pipeline.yml | 117 + .azuredevops/release-pipeline.yml | 66 + .gitignore | 4 + README.md | 94 + audit-go.iml | 15 + audit/api/api.go | 292 +++ audit/api/api_common.go | 278 +++ audit/api/api_common_test.go | 413 +++ audit/api/api_legacy.go | 164 ++ audit/api/api_legacy_converter.go | 312 +++ audit/api/api_legacy_converter_test.go | 21 + audit/api/api_legacy_dynamic.go | 160 ++ audit/api/api_legacy_dynamic_test.go | 528 ++++ audit/api/api_legacy_test.go | 671 +++++ audit/api/api_mock.go | 111 + audit/api/api_mock_test.go | 61 + audit/api/api_routable.go | 198 ++ audit/api/api_routable_test.go | 574 +++++ audit/api/base64.go | 70 + audit/api/base64_test.go | 85 + audit/api/builder.go | 662 +++++ audit/api/builder_test.go | 1167 +++++++++ audit/api/converter.go | 32 + audit/api/log.go | 100 + audit/api/log_test.go | 101 + audit/api/model.go | 983 ++++++++ audit/api/model_test.go | 1121 +++++++++ audit/api/schema_validation_test.go | 128 + audit/api/test_data.go | 462 ++++ audit/messaging/messaging.go | 218 ++ audit/messaging/messaging_test.go | 163 ++ audit/messaging/solace.go | 438 ++++ audit/utils/sequence_generator.go | 45 + audit/utils/sequence_generator_test.go | 22 + buf.lock | 2 + gen/go/audit/v1/audit_event.pb.go | 1954 +++++++++++++++ gen/go/audit/v1/audit_event.pb.validate.go | 2209 +++++++++++++++++ gen/go/audit/v1/routable_event.pb.go | 543 ++++ gen/go/audit/v1/routable_event.pb.validate.go | 574 +++++ go.mod | 73 + go.sum | 223 ++ log/log.go | 27 + log/log_test.go | 40 + log/slog.go | 35 + log/slog_test.go | 43 + log/zerolog.go | 25 + log/zerolog_test.go | 42 + proto/audit/v1/audit_event.proto | 632 +++++ proto/audit/v1/routable_event.proto | 135 + proto/buf.gen.yaml | 11 + proto/buf.lock | 8 + proto/buf.yaml | 9 + telemetry/telemetry.go | 24 + 53 files changed, 16485 insertions(+) create mode 100644 .azuredevops/build-pipeline.yml create mode 100644 .azuredevops/release-pipeline.yml create mode 100644 README.md create mode 100644 audit-go.iml create mode 100644 audit/api/api.go create mode 100644 audit/api/api_common.go create mode 100644 audit/api/api_common_test.go create mode 100644 audit/api/api_legacy.go create mode 100644 audit/api/api_legacy_converter.go create mode 100644 audit/api/api_legacy_converter_test.go create mode 100644 audit/api/api_legacy_dynamic.go create mode 100644 audit/api/api_legacy_dynamic_test.go create mode 100644 audit/api/api_legacy_test.go create mode 100644 audit/api/api_mock.go create mode 100644 audit/api/api_mock_test.go create mode 100644 audit/api/api_routable.go create mode 100644 audit/api/api_routable_test.go create mode 100644 audit/api/base64.go create mode 100644 audit/api/base64_test.go create mode 100644 audit/api/builder.go create mode 100644 audit/api/builder_test.go create mode 100644 audit/api/converter.go create mode 100644 audit/api/log.go create mode 100644 audit/api/log_test.go create mode 100644 audit/api/model.go create mode 100644 audit/api/model_test.go create mode 100644 audit/api/schema_validation_test.go create mode 100644 audit/api/test_data.go create mode 100644 audit/messaging/messaging.go create mode 100644 audit/messaging/messaging_test.go create mode 100644 audit/messaging/solace.go create mode 100644 audit/utils/sequence_generator.go create mode 100644 audit/utils/sequence_generator_test.go create mode 100644 buf.lock create mode 100644 gen/go/audit/v1/audit_event.pb.go create mode 100644 gen/go/audit/v1/audit_event.pb.validate.go create mode 100644 gen/go/audit/v1/routable_event.pb.go create mode 100644 gen/go/audit/v1/routable_event.pb.validate.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 log/log.go create mode 100644 log/log_test.go create mode 100644 log/slog.go create mode 100644 log/slog_test.go create mode 100644 log/zerolog.go create mode 100644 log/zerolog_test.go create mode 100644 proto/audit/v1/audit_event.proto create mode 100644 proto/audit/v1/routable_event.proto create mode 100644 proto/buf.gen.yaml create mode 100644 proto/buf.lock create mode 100644 proto/buf.yaml create mode 100644 telemetry/telemetry.go 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 +}