mirror of
https://dev.azure.com/schwarzit/schwarzit.stackit-public/_git/audit-go
synced 2026-02-07 16:47:24 +00:00
Merged PR 666097: feat: Add implementation of core library
Related work items: #687250
This commit is contained in:
parent
b7171c2177
commit
9337231a6f
53 changed files with 16485 additions and 0 deletions
117
.azuredevops/build-pipeline.yml
Normal file
117
.azuredevops/build-pipeline.yml
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
variables:
|
||||
- name: bufVersion
|
||||
value: v1.45.0
|
||||
- name: golangCiLintVersion
|
||||
value: v1.61.0
|
||||
- name: goVersion
|
||||
value: 1.23.2
|
||||
- name: protobufValidateVersion
|
||||
value: v1.1.0
|
||||
- name: protobufVersion
|
||||
value: v1.35.1
|
||||
- name: GOPATH
|
||||
value: '$(system.defaultWorkingDirectory)/gopath'
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
jobs:
|
||||
- job: GoBuildTest
|
||||
displayName: Run build and tests
|
||||
variables:
|
||||
- name: isCiBuild
|
||||
value: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')]
|
||||
steps:
|
||||
- task: GoTool@0
|
||||
displayName: Install Go $(goVersion)
|
||||
inputs:
|
||||
version: $(goVersion)
|
||||
|
||||
- bash: |
|
||||
set -e
|
||||
go env -w GOMODCACHE="$(pwd)/.gomodcache"
|
||||
displayName: Configure GOMODCACHE
|
||||
|
||||
- bash: |
|
||||
set -e
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@$(protobufVersion)
|
||||
go install github.com/envoyproxy/protoc-gen-validate@$(protobufValidateVersion)
|
||||
go install github.com/bufbuild/buf/cmd/buf@$(bufVersion)
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin $(golangCiLintVersion)
|
||||
condition: succeeded()
|
||||
displayName: Install build dependencies
|
||||
|
||||
- bash: |
|
||||
set -e
|
||||
echo on
|
||||
go mod download
|
||||
go mod tidy
|
||||
go get ./...
|
||||
condition: succeeded()
|
||||
displayName: Download dependencies
|
||||
|
||||
- bash: |
|
||||
set -e
|
||||
echo on
|
||||
rm -rf gen/
|
||||
export PATH="$PATH:$GOPATH/bin"
|
||||
buf format proto -w
|
||||
cd proto
|
||||
buf lint
|
||||
buf generate
|
||||
cd -
|
||||
condition: succeeded()
|
||||
displayName: Regenerate code from schema
|
||||
|
||||
- bash: |
|
||||
set -e
|
||||
echo on
|
||||
export PATH="$PATH:$GOPATH/bin"
|
||||
go fmt ./... && go vet ./... && golangci-lint run
|
||||
condition: succeeded()
|
||||
displayName: Format and lint
|
||||
|
||||
- bash: |
|
||||
set -e
|
||||
echo on
|
||||
git diff HEAD --name-only --exit-code
|
||||
condition: succeeded()
|
||||
displayName: Check local changes after code generation and formatting
|
||||
|
||||
- bash: go build ./...
|
||||
condition: succeeded()
|
||||
displayName: Build
|
||||
|
||||
- bash: go test ./...
|
||||
condition: succeeded()
|
||||
displayName: Run tests
|
||||
|
||||
- task: SnykSecurityScan@1
|
||||
condition: and(succeeded(), eq(variables.isCiBuild, true))
|
||||
displayName: Snyk check (main branch)
|
||||
inputs:
|
||||
additionalArguments: "--remote-repo-url=$(Build.Repository.Uri)"
|
||||
failOnIssues: false
|
||||
monitorWhen: 'always'
|
||||
organization: 'xx-sit-odj-stackit-public'
|
||||
projectName: $(Build.Repository.Name)
|
||||
serviceConnectionEndpoint: 'xx-sit-odj-stackit-public-snyk'
|
||||
testType: 'app'
|
||||
|
||||
- task: SnykSecurityScan@1
|
||||
condition: and(succeeded(), eq(variables.isCiBuild, false))
|
||||
displayName: Snyk check
|
||||
inputs:
|
||||
additionalArguments: "--remote-repo-url=$(Build.Repository.Uri)"
|
||||
failOnIssues: false
|
||||
monitorWhen: 'never'
|
||||
organization: 'xx-sit-odj-stackit-public'
|
||||
projectName: $(Build.Repository.Name)
|
||||
serviceConnectionEndpoint: 'xx-sit-odj-stackit-public-snyk'
|
||||
testType: 'app'
|
||||
|
||||
- bash: sudo rm -rf .gomodcache
|
||||
condition: always()
|
||||
displayName: Clean up the local cache (.gomodcache)
|
||||
66
.azuredevops/release-pipeline.yml
Normal file
66
.azuredevops/release-pipeline.yml
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
trigger: none
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
parameters:
|
||||
- name: releaseType
|
||||
displayName: Type of the release
|
||||
type: string
|
||||
default: minor
|
||||
values:
|
||||
- major
|
||||
- minor
|
||||
- patch
|
||||
|
||||
stages:
|
||||
- stage: Release
|
||||
variables:
|
||||
- name: isMainBranch
|
||||
value: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')]
|
||||
jobs:
|
||||
- job: Release
|
||||
condition: eq(variables.isMainBranch, true)
|
||||
displayName: Release
|
||||
steps:
|
||||
- checkout: self
|
||||
persistCredentials: true
|
||||
fetchDepth: 0
|
||||
|
||||
- bash: |
|
||||
set -e
|
||||
RELEASE_TYPE="${{ parameters.releaseType }}"
|
||||
|
||||
TAG_VERSION=$(git describe --tags --abbrev=0 --match v\*)
|
||||
VERSION_NUMBER=$(echo ${TAG_VERSION} | sed 's/v//g' | sed 's/-.*//g')
|
||||
|
||||
MAJOR=$(echo ${VERSION_NUMBER} | cut -d. -f1)
|
||||
MINOR=$(echo ${VERSION_NUMBER} | cut -d. -f2)
|
||||
PATCH=$(echo ${VERSION_NUMBER} | cut -d. -f3)
|
||||
|
||||
echo "Current version ${VERSION_NUMBER}"
|
||||
echo "Major version: ${MAJOR}"
|
||||
echo "Minor version: ${MINOR}"
|
||||
echo "Patch version: ${PATCH}"
|
||||
|
||||
if [ "${RELEASE_TYPE}" == "major" ]; then
|
||||
RELEASE_VERSION=$((MAJOR + 1)).0.0
|
||||
elif [ "${RELEASE_TYPE}" == "minor" ]; then
|
||||
RELEASE_VERSION=${MAJOR}.$((MINOR + 1)).0
|
||||
elif [ "${RELEASE_TYPE}" == "patch" ]; then
|
||||
RELEASE_VERSION=${MAJOR}.${MINOR}.$((PATCH + 1))
|
||||
else
|
||||
echo "No release type specified"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
RELEASE_VERSION="v${RELEASE_VERSION}"
|
||||
echo "Release version: ${RELEASE_VERSION}"
|
||||
|
||||
COMMIT_ID=$(git rev-parse HEAD)
|
||||
echo "Commit: ${COMMIT_ID}"
|
||||
|
||||
git tag ${RELEASE_VERSION} ${COMMIT_ID}
|
||||
git push origin ${RELEASE_VERSION}
|
||||
|
||||
displayName: Release new ${{ parameters.releaseType }} version
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -170,3 +170,7 @@ fabric.properties
|
|||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Buf
|
||||
gen/java
|
||||
gen/python
|
||||
94
README.md
Normal file
94
README.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
## audit-go
|
||||
|
||||
The audit-go library is the core library for validation and sending of audit events.
|
||||
|
||||
### API Documentation
|
||||
|
||||
The api documentation can be found
|
||||
[here](https://developers.stackit.schwarz/domains/core-platform/audit-log/sdk/overview/).
|
||||
|
||||
### Supported data types for routing
|
||||
|
||||
The following data types are currently supported for routing.
|
||||
|
||||
| ObjectType | Routable to customer | Description |
|
||||
|--------------|----------------------|----------------------|
|
||||
| system | no | The STACKIT system |
|
||||
| project | yes | STACKIT project |
|
||||
| organization | yes | STACKIT organization |
|
||||
| folder | yes | STACKIT folder |
|
||||
|
||||
### Additional API implementations
|
||||
|
||||
There's already an implementation draft of the api for the new dynamically routing
|
||||
audit log solution. As the implementation of the system has not officially been
|
||||
started yet, it's only a draft with integration tests.
|
||||
The API code is private to not confuse users or loose data until the new system
|
||||
is ready to be used.
|
||||
|
||||
The code can be found in the [api_routable.go](./api_routable.go) and
|
||||
[api_routable_test.go](./api_routable_test.go) files.
|
||||
|
||||
### Development
|
||||
|
||||
#### Linter
|
||||
|
||||
The linter *golangci-lint* can either be installed via package manager (e.g. brew) or
|
||||
by running the following command in the terminal:
|
||||
|
||||
```shell
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.61.0
|
||||
```
|
||||
|
||||
#### Schema Generation
|
||||
|
||||
Go structs are generated from Protobuf schema by using [Buf](https://buf.build) and some plugins.
|
||||
The buf plugins are referenced in the *proto/buf.gen.yaml* file and are expected
|
||||
to be installed locally.
|
||||
The schema generator also generates code to validate constraints specified
|
||||
in the schema.
|
||||
|
||||
Buf and the required plugins can either be installed via package manager (e.g. brew)
|
||||
or manually by running:
|
||||
|
||||
```shell
|
||||
go install github.com/bufbuild/buf/cmd/buf@v1.45.0 #Pipeline: bufVersion
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.35.1 #Pipeline: protobufVersion, go.mod: buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go
|
||||
go install github.com/envoyproxy/protoc-gen-validate@v1.1.0 #Pipeline: protobufValidateVersion, go.mod: google.golang.org/protobuf
|
||||
```
|
||||
|
||||
Please check that the versions above match the versions in the *go.mod* file
|
||||
and the *.azuredevops/build-pipeline.yml* file.
|
||||
|
||||
Then the schema can be generated:
|
||||
|
||||
```bash
|
||||
cd proto
|
||||
buf generate
|
||||
```
|
||||
|
||||
#### Build
|
||||
|
||||
The library can be built by executing the following commands:
|
||||
|
||||
```bash
|
||||
go mod download && go mod tidy && go get ./... && go fmt ./... && go vet ./... && golangci-lint run && go build ./... && go test ./...
|
||||
```
|
||||
|
||||
##### Testcontainers
|
||||
|
||||
To run the tests **Docker** is needed as [Testcontainers](https://testcontainers.com/)
|
||||
is used to run integration tests using a solace docker container.
|
||||
|
||||
#### Register buf validation schema in IntelliJ / Goland
|
||||
|
||||
The schema files use `Buf` protobuf extensions for validation of constraints.
|
||||
|
||||
To register the schema in IntelliJ / Goland clone the repo and add the import path:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/bufbuild/protovalidate.git
|
||||
```
|
||||
|
||||
IntelliJ/Goland > Settings > Languages & Frameworks > Protocol Buffers > Import Paths > +
|
||||
(Add Path) > …/protovalidate/proto/protovalidate
|
||||
15
audit-go.iml
Normal file
15
audit-go.iml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="Python" name="Python">
|
||||
<configuration sdkName="Python 3.9 (kafka)" />
|
||||
</facet>
|
||||
</component>
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Python 3.9 (kafka) interpreter library" level="application" />
|
||||
</component>
|
||||
</module>
|
||||
292
audit/api/api.go
Normal file
292
audit/api/api.go
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
EventTypeAdminActivity EventType = "admin-activity"
|
||||
EventTypeSystemEvent EventType = "system-event"
|
||||
EventTypePolicyDenied EventType = "policy-denied"
|
||||
EventTypeDataAccess EventType = "data-access"
|
||||
)
|
||||
|
||||
type ObjectType string
|
||||
|
||||
const (
|
||||
ObjectTypeSystem ObjectType = "system"
|
||||
ObjectTypeOrganization ObjectType = "organization"
|
||||
ObjectTypeFolder ObjectType = "folder"
|
||||
ObjectTypeProject ObjectType = "project"
|
||||
)
|
||||
|
||||
func ObjectTypeFromPluralString(value string) ObjectType {
|
||||
pluralSuffix := "s"
|
||||
switch value {
|
||||
case string(ObjectTypeOrganization) + pluralSuffix:
|
||||
return ObjectTypeOrganization
|
||||
case string(ObjectTypeFolder) + pluralSuffix:
|
||||
return ObjectTypeFolder
|
||||
case string(ObjectTypeProject) + pluralSuffix:
|
||||
return ObjectTypeProject
|
||||
case string(ObjectTypeSystem):
|
||||
return ObjectTypeSystem
|
||||
default:
|
||||
return ObjectType(value)
|
||||
}
|
||||
}
|
||||
|
||||
func (t ObjectType) IsSupportedType() error {
|
||||
switch t {
|
||||
case ObjectTypeOrganization:
|
||||
fallthrough
|
||||
case ObjectTypeFolder:
|
||||
fallthrough
|
||||
case ObjectTypeProject:
|
||||
fallthrough
|
||||
case ObjectTypeSystem:
|
||||
return nil
|
||||
default:
|
||||
return ErrUnknownObjectType
|
||||
}
|
||||
}
|
||||
|
||||
func (t ObjectType) Plural() string {
|
||||
pluralSuffix := "s"
|
||||
switch t {
|
||||
case ObjectTypeOrganization:
|
||||
return string(ObjectTypeOrganization) + pluralSuffix
|
||||
case ObjectTypeFolder:
|
||||
return string(ObjectTypeFolder) + pluralSuffix
|
||||
case ObjectTypeProject:
|
||||
return string(ObjectTypeProject) + pluralSuffix
|
||||
case ObjectTypeSystem:
|
||||
return string(ObjectTypeSystem)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
var SystemIdentifier = &auditV1.ObjectIdentifier{Identifier: uuid.Nil.String(), Type: string(ObjectTypeSystem)}
|
||||
var RoutableSystemIdentifier = NewRoutableIdentifier(SystemIdentifier)
|
||||
|
||||
// AuditApi is the interface to log audit events.
|
||||
//
|
||||
// It provides a Log method that can be used to validate and directly send events.
|
||||
// If the transactional outbox pattern should be used, the ValidateAndSerialize and Send methods
|
||||
// can be called manually to decouple operations.
|
||||
type AuditApi interface {
|
||||
|
||||
// Log is a convenience method that validates, serializes and sends data over the wire.
|
||||
// If the transactional outbox pattern should be used, the ValidateAndSerialize method
|
||||
// and Send method can be called separately.
|
||||
// If an error is returned it is the responsibility of the caller to retry. The api does
|
||||
// not store, buffer events or retry failed invocation automatically.
|
||||
//
|
||||
// Parameters:
|
||||
// * ctx - the context object
|
||||
// * event - the auditV1.AuditEvent
|
||||
// * visibility - route the event only internally or to the customer (no routing in the legacy solution)
|
||||
// * routableIdentifier - the identifier of the object
|
||||
//
|
||||
// Returns:
|
||||
// * an error if the validation, serialization or send failed
|
||||
Log(
|
||||
ctx context.Context,
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
) error
|
||||
|
||||
// LogWithTrace is a convenience method that validates, serializes and sends data over the wire.
|
||||
// If the transactional outbox pattern should be used, the ValidateAndSerializeWithTrace method
|
||||
// and Send method can be called separately. The method accepts traceParent and traceState
|
||||
// parameters to put into attributes of the AuditLogEntry.
|
||||
// If an error is returned it is the responsibility of the caller to retry. The api does not store,
|
||||
// buffer events or retry failed invocation automatically.
|
||||
//
|
||||
// Parameters:
|
||||
// * ctx - the context object
|
||||
// * event - the auditV1.AuditEvent
|
||||
// * visibility - route the event only internally or to the customer (no routing in the legacy solution)
|
||||
// * routableIdentifier - the identifier of the object
|
||||
// * traceParent - optional trace parent
|
||||
// * traceState - optional trace state
|
||||
//
|
||||
// Returns:
|
||||
// * an error if the validation, serialization or send failed
|
||||
LogWithTrace(
|
||||
ctx context.Context,
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
traceParent *string,
|
||||
traceState *string,
|
||||
) error
|
||||
|
||||
// ValidateAndSerialize validates and serializes the event into a byte representation.
|
||||
// The result has to be sent explicitly by calling the Send method.
|
||||
//
|
||||
// Parameters:
|
||||
// * event - the auditV1.AuditEvent
|
||||
// * visibility - route the event only internally or to the customer (no routing in the legacy solution)
|
||||
// * routableIdentifier - the identifier of the object
|
||||
//
|
||||
// Returns:
|
||||
// * the CloudEvent (i.e. the serialized AuditLogEntry with metadata)
|
||||
// * an error if validation or serialization failed
|
||||
ValidateAndSerialize(
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
) (*CloudEvent, error)
|
||||
|
||||
// ValidateAndSerializeWithTrace validates and serializes the event into a byte representation.
|
||||
// The result has to be sent explicitly by calling the Send method.
|
||||
//
|
||||
// Parameters:
|
||||
// * event - the auditV1.AuditEvent
|
||||
// * visibility - route the event only internally or to the customer (no routing in the legacy solution)
|
||||
// * routableIdentifier - the identifier of the object
|
||||
// * traceParent - optional trace parent
|
||||
// * traceState - optional trace state
|
||||
//
|
||||
// Returns:
|
||||
// * the CloudEvent (i.e. the serialized AuditLogEntry with metadata)
|
||||
// * an error if validation or serialization failed
|
||||
ValidateAndSerializeWithTrace(
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
traceParent *string,
|
||||
traceState *string,
|
||||
) (*CloudEvent, error)
|
||||
|
||||
// Send the serialized content as byte array to the audit log system.
|
||||
// If an error is returned it is the responsibility of the caller to
|
||||
// retry. The api does not store, buffer events or retry failed
|
||||
// invocation automatically.
|
||||
//
|
||||
// Parameters:
|
||||
// * ctx - the context object
|
||||
// * routableIdentifier - the identifier of the object
|
||||
// * cloudEvent - the serialized AuditLogEntry with metadata
|
||||
//
|
||||
// Returns:
|
||||
// * an error if the event could not be sent
|
||||
Send(
|
||||
ctx context.Context,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
cloudEvent *CloudEvent,
|
||||
) error
|
||||
}
|
||||
|
||||
// ProtobufValidator is an abstraction for validators.
|
||||
// Concrete implementations are e.g. protovalidate.Validator
|
||||
type ProtobufValidator interface {
|
||||
Validate(msg proto.Message) error
|
||||
}
|
||||
|
||||
// CloudEvent is a representation of a cloudevents.io object.
|
||||
//
|
||||
// More information about the schema and attribute semantics can be found here:
|
||||
// https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#required-attributes
|
||||
type CloudEvent struct {
|
||||
|
||||
// The version of the CloudEvents specification which the event uses.
|
||||
// This enables the interpretation of the context. Compliant event producers MUST use a value of 1.0
|
||||
// when referring to this version of the specification.
|
||||
//
|
||||
// Currently, this attribute will only have the 'major' and 'minor' version numbers included in it.
|
||||
// This allows for 'patch' changes to the specification to be made without changing this property's
|
||||
// value in the serialization.
|
||||
SpecVersion string
|
||||
|
||||
// The source system uri-reference. Producers MUST ensure that source + id is unique for each distinct event.
|
||||
Source string
|
||||
|
||||
// Identifier of the event. Producers MUST ensure that source + id is unique for each distinct event.
|
||||
// If a duplicate event is re-sent (e.g. due to a network error) it MAY have the same id.
|
||||
// Consumers MAY assume that Events with identical source and id are duplicates.
|
||||
Id string
|
||||
|
||||
// The time when the event happened
|
||||
Time time.Time
|
||||
|
||||
// The content type of the payload
|
||||
// Examples could be:
|
||||
// - application/cloudevents+json
|
||||
// - application/cloudevents+json; charset=UTF-8
|
||||
// - application/cloudevents-batch+json
|
||||
// - application/cloudevents+protobuf
|
||||
// - application/cloudevents+avro
|
||||
// Source: https://github.com/cloudevents/spec/blob/main/cloudevents/formats/protobuf-format.md
|
||||
DataContentType string
|
||||
|
||||
// The object type (i.e. the fully qualified protobuf type name)
|
||||
DataType string
|
||||
|
||||
// The identifier of the referring object.
|
||||
Subject string
|
||||
|
||||
// The serialized payload
|
||||
Data []byte
|
||||
|
||||
// Optional W3C conform trace parent:
|
||||
// https://www.w3.org/TR/trace-context/#traceparent-header
|
||||
//
|
||||
// Format: <version>-<trace-id>-<parent-id>-<trace-flags>
|
||||
//
|
||||
// Examples:
|
||||
// "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
|
||||
TraceParent *string
|
||||
|
||||
// Optional W3C conform trace state header:
|
||||
// https://www.w3.org/TR/trace-context/#tracestate-header
|
||||
//
|
||||
// Format: <key1>=<value1>[,<keyN>=<valueN>]
|
||||
//
|
||||
// Examples:
|
||||
// "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"
|
||||
TraceState *string
|
||||
}
|
||||
|
||||
// TopicNameResolver is an abstraction for dynamic topic name resolution
|
||||
// based on event data or api parameters.
|
||||
type TopicNameResolver interface {
|
||||
|
||||
// Resolve returns a topic name for the given object identifier
|
||||
Resolve(routableIdentifier *RoutableIdentifier) (string, error)
|
||||
}
|
||||
|
||||
type RoutableIdentifier struct {
|
||||
Identifier string
|
||||
Type ObjectType
|
||||
}
|
||||
|
||||
func NewRoutableIdentifier(objectIdentifier *auditV1.ObjectIdentifier) *RoutableIdentifier {
|
||||
if objectIdentifier == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &RoutableIdentifier{
|
||||
Identifier: objectIdentifier.Identifier,
|
||||
Type: ObjectType(objectIdentifier.Type),
|
||||
}
|
||||
}
|
||||
|
||||
func (r RoutableIdentifier) ToObjectIdentifier() *auditV1.ObjectIdentifier {
|
||||
return &auditV1.ObjectIdentifier{
|
||||
Identifier: r.Identifier,
|
||||
Type: string(r.Type),
|
||||
}
|
||||
}
|
||||
278
audit/api/api_common.go
Normal file
278
audit/api/api_common.go
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/telemetry"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/messaging"
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// ContentTypeCloudEventsProtobuf the cloudevents protobuf content-type sent in metadata of messages
|
||||
const ContentTypeCloudEventsProtobuf = "application/cloudevents+protobuf"
|
||||
const ContentTypeCloudEventsJson = "application/cloudevents+json; charset=UTF-8"
|
||||
|
||||
// ErrAttributeIdentifierInvalid indicates that the object identifier
|
||||
// and the identifier in the checked attribute do not match
|
||||
var ErrAttributeIdentifierInvalid = errors.New("attribute identifier invalid")
|
||||
|
||||
// ErrAttributeTypeInvalid indicates that an invalid type has been provided.
|
||||
var ErrAttributeTypeInvalid = errors.New("attribute type invalid")
|
||||
|
||||
// ErrCloudEventNil states that the given cloud event is nil
|
||||
var ErrCloudEventNil = errors.New("cloud event nil")
|
||||
|
||||
// ErrEventNil indicates that the event was nil
|
||||
var ErrEventNil = errors.New("event is nil")
|
||||
|
||||
// ErrInvalidRoutableIdentifierForSystemEvent states that the routable identifier is not valid for a system event
|
||||
var ErrInvalidRoutableIdentifierForSystemEvent = errors.New("invalid identifier for system event")
|
||||
|
||||
// ErrMessagingApiNil states that the messaging api is nil
|
||||
var ErrMessagingApiNil = errors.New("messaging api nil")
|
||||
|
||||
// ErrObjectIdentifierNil indicates that the object identifier was nil
|
||||
var ErrObjectIdentifierNil = errors.New("object identifier is nil")
|
||||
|
||||
// ErrObjectIdentifierVisibilityMismatch indicates that a reference mismatch was detected.
|
||||
//
|
||||
// Valid combinations are:
|
||||
// * Visibility: Public, ObjectIdentifier: <type>
|
||||
// * Visibility: Private, ObjectIdentifier: <type | system>
|
||||
var ErrObjectIdentifierVisibilityMismatch = errors.New("object reference visibility mismatch")
|
||||
|
||||
// ErrTopicNameResolverNil states that the topic name resolve is nil
|
||||
var ErrTopicNameResolverNil = errors.New("topic name resolver nil")
|
||||
|
||||
// ErrUnknownObjectType indicates that the given input is an unknown object type
|
||||
var ErrUnknownObjectType = errors.New("unknown object type")
|
||||
|
||||
// ErrUnsupportedEventTypeDataAccess states that the event type "data-access" is currently not supported
|
||||
var ErrUnsupportedEventTypeDataAccess = errors.New("unsupported event type data access")
|
||||
|
||||
// ErrUnsupportedObjectIdentifierType indicates that an unsupported object identifier type has been provided
|
||||
var ErrUnsupportedObjectIdentifierType = errors.New("unsupported object identifier type")
|
||||
|
||||
// ErrUnsupportedRoutableType indicates that the given input is an unsupported routable type
|
||||
var ErrUnsupportedRoutableType = errors.New("unsupported routable type")
|
||||
|
||||
func validateAndSerializePartially(
|
||||
validator *ProtobufValidator,
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
) (*auditV1.RoutableAuditEvent, error) {
|
||||
|
||||
// Check preconditions
|
||||
err := validateAuditLogEntry(validator, event, visibility, routableIdentifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Serialize the AuditLogEntry and wrap it into a RoutableAuditEvent
|
||||
routableEvent, err := newValidatedRoutableAuditEvent(validator, event, visibility, routableIdentifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return routableEvent, nil
|
||||
}
|
||||
|
||||
func newValidatedRoutableAuditEvent(
|
||||
validator *ProtobufValidator,
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier) (*auditV1.RoutableAuditEvent, error) {
|
||||
|
||||
// Test serialization even if the data is dropped later when logging to the legacy solution
|
||||
auditEventBytes, err := proto.Marshal(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payload := auditV1.UnencryptedData{
|
||||
Data: auditEventBytes,
|
||||
ProtobufType: fmt.Sprintf("%v", event.ProtoReflect().Descriptor().FullName()),
|
||||
}
|
||||
|
||||
routableEvent := auditV1.RoutableAuditEvent{
|
||||
OperationName: event.ProtoPayload.OperationName,
|
||||
ObjectIdentifier: routableIdentifier.ToObjectIdentifier(),
|
||||
Visibility: visibility,
|
||||
Data: &auditV1.RoutableAuditEvent_UnencryptedData{UnencryptedData: &payload},
|
||||
}
|
||||
|
||||
err = (*validator).Validate(&routableEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &routableEvent, nil
|
||||
}
|
||||
|
||||
func validateAuditLogEntry(
|
||||
validator *ProtobufValidator,
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
) error {
|
||||
|
||||
// Return error if the given event or object identifier is nil
|
||||
if event == nil {
|
||||
return ErrEventNil
|
||||
}
|
||||
if routableIdentifier == nil {
|
||||
return ErrObjectIdentifierNil
|
||||
}
|
||||
|
||||
// Validate the actual event
|
||||
err := (*validator).Validate(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure that a valid object identifier is set if the event is public
|
||||
if isSystemIdentifier(routableIdentifier) && visibility == auditV1.Visibility_VISIBILITY_PUBLIC {
|
||||
return ErrObjectIdentifierVisibilityMismatch
|
||||
}
|
||||
|
||||
// Check that provided identifier type is supported
|
||||
if err := routableIdentifier.Type.IsSupportedType(); err != nil {
|
||||
if errors.Is(err, ErrUnknownObjectType) {
|
||||
return ErrUnsupportedRoutableType
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Check identifier consistency across event attributes
|
||||
if strings.HasSuffix(event.LogName, string(EventTypeSystemEvent)) {
|
||||
if !(routableIdentifier.Identifier == SystemIdentifier.Identifier && routableIdentifier.Type == ObjectTypeSystem) {
|
||||
return ErrInvalidRoutableIdentifierForSystemEvent
|
||||
}
|
||||
// The resource name can either contain the system identifier or another resource identifier
|
||||
} else {
|
||||
if err := areIdentifiersIdentical(routableIdentifier, event.LogName); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := areIdentifiersIdentical(routableIdentifier, event.ProtoPayload.ResourceName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send implements AuditApi.Send
|
||||
func send(
|
||||
topicNameResolver *TopicNameResolver,
|
||||
messagingApi *messaging.Api,
|
||||
ctx context.Context,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
cloudEvent *CloudEvent,
|
||||
) error {
|
||||
|
||||
// Check that given objects are not nil
|
||||
if topicNameResolver == nil {
|
||||
return ErrTopicNameResolverNil
|
||||
}
|
||||
if messagingApi == nil {
|
||||
return ErrMessagingApiNil
|
||||
}
|
||||
if cloudEvent == nil {
|
||||
return ErrCloudEventNil
|
||||
}
|
||||
if routableIdentifier == nil {
|
||||
return ErrObjectIdentifierNil
|
||||
}
|
||||
|
||||
// Check that provided identifier type is supported
|
||||
if err := routableIdentifier.Type.IsSupportedType(); err != nil {
|
||||
if errors.Is(err, ErrUnknownObjectType) {
|
||||
return ErrUnsupportedRoutableType
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
topic, err := (*topicNameResolver).Resolve(routableIdentifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Naming according to AMQP protocol binding spec
|
||||
// https://github.com/cloudevents/spec/blob/main/cloudevents/bindings/amqp-protocol-binding.md
|
||||
applicationAttributes := make(map[string]any)
|
||||
applicationAttributes["cloudEvents:specversion"] = cloudEvent.SpecVersion
|
||||
applicationAttributes["cloudEvents:source"] = cloudEvent.Source
|
||||
applicationAttributes["cloudEvents:id"] = cloudEvent.Id
|
||||
applicationAttributes["cloudEvents:time"] = cloudEvent.Time.UnixMilli()
|
||||
applicationAttributes["cloudEvents:datacontenttype"] = cloudEvent.DataContentType
|
||||
applicationAttributes["cloudEvents:type"] = cloudEvent.DataType
|
||||
applicationAttributes["cloudEvents:subject"] = cloudEvent.Subject
|
||||
if cloudEvent.TraceParent != nil {
|
||||
applicationAttributes["cloudEvents:traceparent"] = *cloudEvent.TraceParent
|
||||
}
|
||||
if cloudEvent.TraceState != nil {
|
||||
applicationAttributes["cloudEvents:tracestate"] = *cloudEvent.TraceState
|
||||
}
|
||||
|
||||
// Telemetry
|
||||
applicationAttributes["cloudEvents:sdklanguage"] = "go"
|
||||
auditGoVersion := telemetry.AuditGoVersion
|
||||
if auditGoVersion != "" {
|
||||
applicationAttributes["cloudEvents:sdkversion"] = auditGoVersion
|
||||
}
|
||||
auditGoGrpcVersion := telemetry.AuditGoGrpcVersion
|
||||
if auditGoGrpcVersion != "" {
|
||||
applicationAttributes["cloudEvents:sdkgrpcversion"] = auditGoGrpcVersion
|
||||
}
|
||||
auditGoHttpVersion := telemetry.AuditGoHttpVersion
|
||||
if auditGoHttpVersion != "" {
|
||||
applicationAttributes["cloudEvents:sdkhttpversion"] = auditGoHttpVersion
|
||||
}
|
||||
|
||||
return (*messagingApi).Send(
|
||||
ctx,
|
||||
topic,
|
||||
(*cloudEvent).Data,
|
||||
(*cloudEvent).DataContentType,
|
||||
applicationAttributes)
|
||||
}
|
||||
|
||||
func isSystemIdentifier(identifier *RoutableIdentifier) bool {
|
||||
if identifier.Identifier == uuid.Nil.String() && identifier.Type == ObjectTypeSystem {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func areIdentifiersIdentical(routableIdentifier *RoutableIdentifier, logName string) error {
|
||||
dataType, identifier := getTypeAndIdentifierFromString(logName)
|
||||
objectType := ObjectTypeFromPluralString(dataType)
|
||||
err := objectType.IsSupportedType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return areTypeAndIdentifierIdentical(routableIdentifier, objectType, identifier)
|
||||
}
|
||||
|
||||
func areTypeAndIdentifierIdentical(routableIdentifier *RoutableIdentifier, dataType ObjectType, identifier string) error {
|
||||
if routableIdentifier.Identifier != identifier {
|
||||
return ErrAttributeIdentifierInvalid
|
||||
}
|
||||
if routableIdentifier.Type != dataType {
|
||||
return ErrAttributeTypeInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTypeAndIdentifierFromString(input string) (string, string) {
|
||||
parts := strings.Split(input, "/")
|
||||
dataType := parts[0]
|
||||
identifier := parts[1]
|
||||
return dataType, identifier
|
||||
}
|
||||
413
audit/api/api_common_test.go
Normal file
413
audit/api/api_common_test.go
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/messaging"
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||
|
||||
"github.com/bufbuild/protovalidate-go"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type MessagingApiMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MessagingApiMock) Send(
|
||||
ctx context.Context,
|
||||
topic string,
|
||||
data []byte,
|
||||
contentType string,
|
||||
applicationProperties map[string]any,
|
||||
) error {
|
||||
|
||||
args := m.Called(ctx, topic, data, contentType, applicationProperties)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
type ProtobufValidatorMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *ProtobufValidatorMock) Validate(msg proto.Message) error {
|
||||
args := m.Called(msg)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
type TopicNameResolverMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *TopicNameResolverMock) Resolve(routableIdentifier *RoutableIdentifier) (string, error) {
|
||||
args := m.Called(routableIdentifier)
|
||||
return args.String(0), args.Error(1)
|
||||
}
|
||||
|
||||
func NewValidator(t *testing.T) ProtobufValidator {
|
||||
validator, err := protovalidate.New()
|
||||
var protoValidator ProtobufValidator = validator
|
||||
assert.NoError(t, err)
|
||||
|
||||
return protoValidator
|
||||
}
|
||||
|
||||
func Test_ValidateAndSerializePartially_EventNil(t *testing.T) {
|
||||
validator := NewValidator(t)
|
||||
|
||||
_, err := validateAndSerializePartially(
|
||||
&validator, nil, auditV1.Visibility_VISIBILITY_PUBLIC, nil)
|
||||
|
||||
assert.ErrorIs(t, err, ErrEventNil)
|
||||
}
|
||||
|
||||
func Test_ValidateAndSerializePartially_AuditEventValidationFailed(t *testing.T) {
|
||||
validator := NewValidator(t)
|
||||
|
||||
event, objectIdentifier := newOrganizationAuditEvent(nil)
|
||||
event.LogName = ""
|
||||
|
||||
_, err := validateAndSerializePartially(
|
||||
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier))
|
||||
|
||||
assert.EqualError(t, err, "validation error:\n - log_name: value is required [required]")
|
||||
}
|
||||
|
||||
func Test_ValidateAndSerializePartially_RoutableEventValidationFailed(t *testing.T) {
|
||||
validator := NewValidator(t)
|
||||
|
||||
event, objectIdentifier := newOrganizationAuditEvent(nil)
|
||||
_, err := validateAndSerializePartially(&validator, event, 3, NewRoutableIdentifier(objectIdentifier))
|
||||
|
||||
assert.EqualError(t, err, "validation error:\n - visibility: value must be one of the defined enum values [enum.defined_only]")
|
||||
}
|
||||
|
||||
func Test_ValidateAndSerializePartially_CheckVisibility_Event(t *testing.T) {
|
||||
validator := NewValidator(t)
|
||||
|
||||
event, objectIdentifier := newOrganizationAuditEvent(nil)
|
||||
|
||||
t.Run("Visibility public - object identifier nil", func(t *testing.T) {
|
||||
_, err := validateAndSerializePartially(
|
||||
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, nil)
|
||||
|
||||
assert.ErrorIs(t, err, ErrObjectIdentifierNil)
|
||||
})
|
||||
|
||||
t.Run("Visibility private - object identifier nil", func(t *testing.T) {
|
||||
_, err := validateAndSerializePartially(
|
||||
&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, nil)
|
||||
|
||||
assert.ErrorIs(t, err, ErrObjectIdentifierNil)
|
||||
})
|
||||
|
||||
t.Run("Visibility public - object identifier system", func(t *testing.T) {
|
||||
_, err := validateAndSerializePartially(
|
||||
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
|
||||
|
||||
assert.ErrorIs(t, err, ErrObjectIdentifierVisibilityMismatch)
|
||||
})
|
||||
|
||||
t.Run("Visibility public - object identifier set", func(t *testing.T) {
|
||||
routableEvent, err := validateAndSerializePartially(
|
||||
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier))
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, routableEvent)
|
||||
})
|
||||
|
||||
t.Run("Visibility private - object identifier system", func(t *testing.T) {
|
||||
_, err := validateAndSerializePartially(
|
||||
&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, RoutableSystemIdentifier)
|
||||
|
||||
assert.ErrorIs(t, err, ErrAttributeIdentifierInvalid)
|
||||
})
|
||||
|
||||
t.Run("Visibility private - object identifier set", func(t *testing.T) {
|
||||
routableEvent, err := validateAndSerializePartially(
|
||||
&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, NewRoutableIdentifier(objectIdentifier))
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, routableEvent)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_ValidateAndSerializePartially_CheckVisibility_SystemEvent(t *testing.T) {
|
||||
validator := NewValidator(t)
|
||||
|
||||
event := newSystemAuditEvent(nil)
|
||||
|
||||
t.Run("Visibility public - object identifier nil", func(t *testing.T) {
|
||||
_, err := validateAndSerializePartially(
|
||||
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, nil)
|
||||
|
||||
assert.ErrorIs(t, err, ErrObjectIdentifierNil)
|
||||
})
|
||||
|
||||
t.Run("Visibility private - object identifier nil", func(t *testing.T) {
|
||||
_, err := validateAndSerializePartially(
|
||||
&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, nil)
|
||||
|
||||
assert.ErrorIs(t, err, ErrObjectIdentifierNil)
|
||||
})
|
||||
|
||||
t.Run("Visibility public - object identifier system", func(t *testing.T) {
|
||||
_, err := validateAndSerializePartially(
|
||||
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
|
||||
|
||||
assert.ErrorIs(t, err, ErrObjectIdentifierVisibilityMismatch)
|
||||
})
|
||||
|
||||
t.Run("Visibility public - object identifier set", func(t *testing.T) {
|
||||
_, err := validateAndSerializePartially(
|
||||
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(
|
||||
&auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: string(ObjectTypeOrganization)}))
|
||||
|
||||
assert.ErrorIs(t, err, ErrInvalidRoutableIdentifierForSystemEvent)
|
||||
})
|
||||
|
||||
t.Run("Visibility private - object identifier system", func(t *testing.T) {
|
||||
routableEvent, err := validateAndSerializePartially(
|
||||
&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, RoutableSystemIdentifier)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, routableEvent)
|
||||
})
|
||||
|
||||
t.Run("Visibility private - object identifier set", func(t *testing.T) {
|
||||
_, err := validateAndSerializePartially(
|
||||
&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, NewRoutableIdentifier(
|
||||
&auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: string(ObjectTypeOrganization)}))
|
||||
|
||||
assert.ErrorIs(t, err, ErrInvalidRoutableIdentifierForSystemEvent)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_ValidateAndSerializePartially_UnsupportedIdentifierType(t *testing.T) {
|
||||
validator := NewValidator(t)
|
||||
|
||||
event, objectIdentifier := newFolderAuditEvent(nil)
|
||||
objectIdentifier.Type = "invalid"
|
||||
|
||||
_, err := validateAndSerializePartially(
|
||||
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier))
|
||||
|
||||
assert.ErrorIs(t, err, ErrUnsupportedRoutableType)
|
||||
}
|
||||
|
||||
func Test_ValidateAndSerializePartially_LogNameIdentifierMismatch(t *testing.T) {
|
||||
validator := NewValidator(t)
|
||||
|
||||
event, objectIdentifier := newFolderAuditEvent(nil)
|
||||
parts := strings.Split(event.LogName, "/")
|
||||
identifier := parts[1]
|
||||
|
||||
t.Run("LogName type mismatch", func(t *testing.T) {
|
||||
event.LogName = fmt.Sprintf("projects/%s/logs/admin-activity", identifier)
|
||||
_, err := validateAndSerializePartially(
|
||||
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier))
|
||||
|
||||
assert.ErrorIs(t, err, ErrAttributeTypeInvalid)
|
||||
})
|
||||
|
||||
t.Run("LogName identifier mismatch", func(t *testing.T) {
|
||||
event.LogName = fmt.Sprintf("folders/%s/logs/admin-activity", uuid.NewString())
|
||||
_, err := validateAndSerializePartially(
|
||||
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier))
|
||||
|
||||
assert.ErrorIs(t, err, ErrAttributeIdentifierInvalid)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_ValidateAndSerializePartially_ResourceNameIdentifierMismatch(t *testing.T) {
|
||||
validator := NewValidator(t)
|
||||
|
||||
event, objectIdentifier := newFolderAuditEvent(nil)
|
||||
parts := strings.Split(event.ProtoPayload.ResourceName, "/")
|
||||
identifier := parts[1]
|
||||
|
||||
t.Run("ResourceName type mismatch", func(t *testing.T) {
|
||||
event.ProtoPayload.ResourceName = fmt.Sprintf("projects/%s", identifier)
|
||||
_, err := validateAndSerializePartially(
|
||||
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier))
|
||||
|
||||
assert.ErrorIs(t, err, ErrAttributeTypeInvalid)
|
||||
})
|
||||
|
||||
t.Run("ResourceName identifier mismatch", func(t *testing.T) {
|
||||
event.ProtoPayload.ResourceName = fmt.Sprintf("folders/%s", uuid.NewString())
|
||||
_, err := validateAndSerializePartially(
|
||||
&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier))
|
||||
|
||||
assert.ErrorIs(t, err, ErrAttributeIdentifierInvalid)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_ValidateAndSerializePartially_SystemEvent(t *testing.T) {
|
||||
validator := NewValidator(t)
|
||||
|
||||
event := newSystemAuditEvent(nil)
|
||||
|
||||
routableEvent, err := validateAndSerializePartially(
|
||||
&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, RoutableSystemIdentifier)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, event.LogName, fmt.Sprintf("system/%s/logs/%s", SystemIdentifier.Identifier, EventTypeSystemEvent))
|
||||
assert.True(t, proto.Equal(routableEvent.ObjectIdentifier, SystemIdentifier))
|
||||
}
|
||||
|
||||
func Test_Send_TopicNameResolverNil(t *testing.T) {
|
||||
err := send(nil, nil, context.Background(), nil, nil)
|
||||
assert.ErrorIs(t, err, ErrTopicNameResolverNil)
|
||||
}
|
||||
|
||||
func Test_Send_TopicNameResolutionError(t *testing.T) {
|
||||
expectedError := errors.New("expected error")
|
||||
|
||||
topicNameResolverMock := TopicNameResolverMock{}
|
||||
topicNameResolverMock.On("Resolve", mock.Anything).Return("topic", expectedError)
|
||||
var topicNameResolver TopicNameResolver = &topicNameResolverMock
|
||||
|
||||
var cloudEvent = CloudEvent{}
|
||||
|
||||
var messagingApi messaging.Api = &messaging.AmqpApi{}
|
||||
err := send(&topicNameResolver, &messagingApi, context.Background(), RoutableSystemIdentifier, &cloudEvent)
|
||||
assert.ErrorIs(t, err, expectedError)
|
||||
}
|
||||
|
||||
func Test_Send_MessagingApiNil(t *testing.T) {
|
||||
var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: "test"}
|
||||
err := send(&topicNameResolver, nil, context.Background(), nil, nil)
|
||||
assert.ErrorIs(t, err, ErrMessagingApiNil)
|
||||
}
|
||||
|
||||
func Test_Send_CloudEventNil(t *testing.T) {
|
||||
var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: "test"}
|
||||
var messagingApi messaging.Api = &messaging.AmqpApi{}
|
||||
|
||||
err := send(&topicNameResolver, &messagingApi, context.Background(), nil, nil)
|
||||
assert.ErrorIs(t, err, ErrCloudEventNil)
|
||||
}
|
||||
|
||||
func Test_Send_ObjectIdentifierNil(t *testing.T) {
|
||||
var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: "test"}
|
||||
var messagingApi messaging.Api = &messaging.AmqpApi{}
|
||||
var cloudEvent = CloudEvent{}
|
||||
|
||||
err := send(&topicNameResolver, &messagingApi, context.Background(), nil, &cloudEvent)
|
||||
assert.ErrorIs(t, err, ErrObjectIdentifierNil)
|
||||
}
|
||||
|
||||
func Test_Send_UnsupportedObjectIdentifierType(t *testing.T) {
|
||||
var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: "test"}
|
||||
var messagingApi messaging.Api = &messaging.AmqpApi{}
|
||||
var cloudEvent = CloudEvent{}
|
||||
var objectIdentifier = auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: "unsupported"}
|
||||
|
||||
err := send(&topicNameResolver, &messagingApi, context.Background(), NewRoutableIdentifier(&objectIdentifier), &cloudEvent)
|
||||
assert.ErrorIs(t, err, ErrUnsupportedRoutableType)
|
||||
}
|
||||
|
||||
func Test_Send(t *testing.T) {
|
||||
topicNameResolverMock := TopicNameResolverMock{}
|
||||
topicNameResolverMock.On("Resolve", mock.Anything).Return("topic", nil)
|
||||
var topicNameResolver TopicNameResolver = &topicNameResolverMock
|
||||
|
||||
messagingApiMock := MessagingApiMock{}
|
||||
messagingApiMock.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
var messagingApi messaging.Api = &messagingApiMock
|
||||
|
||||
var cloudEvent = CloudEvent{}
|
||||
assert.NoError(t, send(&topicNameResolver, &messagingApi, context.Background(), RoutableSystemIdentifier, &cloudEvent))
|
||||
assert.True(t, messagingApiMock.AssertNumberOfCalls(t, "Send", 1))
|
||||
}
|
||||
|
||||
func Test_SendAllHeadersSet(t *testing.T) {
|
||||
topicNameResolverMock := TopicNameResolverMock{}
|
||||
topicNameResolverMock.On("Resolve", mock.Anything).Return("topic", nil)
|
||||
var topicNameResolver TopicNameResolver = &topicNameResolverMock
|
||||
|
||||
messagingApiMock := MessagingApiMock{}
|
||||
messagingApiMock.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
var messagingApi messaging.Api = &messagingApiMock
|
||||
|
||||
traceState := "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"
|
||||
traceParent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
|
||||
expectedTime := time.Now()
|
||||
var cloudEvent = CloudEvent{
|
||||
SpecVersion: "1.0",
|
||||
Source: "resourcemanager",
|
||||
Id: "id",
|
||||
Time: expectedTime,
|
||||
DataContentType: ContentTypeCloudEventsProtobuf,
|
||||
DataType: "type",
|
||||
Subject: "subject",
|
||||
TraceParent: &traceParent,
|
||||
TraceState: &traceState,
|
||||
}
|
||||
assert.NoError(t, send(&topicNameResolver, &messagingApi, context.Background(), RoutableSystemIdentifier, &cloudEvent))
|
||||
assert.True(t, messagingApiMock.AssertNumberOfCalls(t, "Send", 1))
|
||||
arguments := messagingApiMock.Calls[0].Arguments
|
||||
topic := arguments.Get(1).(string)
|
||||
assert.Equal(t, "topic", topic)
|
||||
contentType := arguments.Get(3).(string)
|
||||
assert.Equal(t, ContentTypeCloudEventsProtobuf, contentType)
|
||||
applicationProperties := arguments.Get(4).(map[string]any)
|
||||
assert.Equal(t, "1.0", applicationProperties["cloudEvents:specversion"])
|
||||
assert.Equal(t, "resourcemanager", applicationProperties["cloudEvents:source"])
|
||||
assert.Equal(t, "id", applicationProperties["cloudEvents:id"])
|
||||
assert.Equal(t, expectedTime.UnixMilli(), applicationProperties["cloudEvents:time"])
|
||||
assert.Equal(t, ContentTypeCloudEventsProtobuf, applicationProperties["cloudEvents:datacontenttype"])
|
||||
assert.Equal(t, "type", applicationProperties["cloudEvents:type"])
|
||||
assert.Equal(t, "subject", applicationProperties["cloudEvents:subject"])
|
||||
assert.Equal(t, traceParent, applicationProperties["cloudEvents:traceparent"])
|
||||
assert.Equal(t, traceState, applicationProperties["cloudEvents:tracestate"])
|
||||
messagingApiMock.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_SendWithoutOptionalHeadersSet(t *testing.T) {
|
||||
topicNameResolverMock := TopicNameResolverMock{}
|
||||
topicNameResolverMock.On("Resolve", mock.Anything).Return("topic", nil)
|
||||
var topicNameResolver TopicNameResolver = &topicNameResolverMock
|
||||
|
||||
messagingApiMock := MessagingApiMock{}
|
||||
messagingApiMock.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
var messagingApi messaging.Api = &messagingApiMock
|
||||
|
||||
expectedTime := time.Now()
|
||||
var cloudEvent = CloudEvent{
|
||||
SpecVersion: "1.0",
|
||||
Source: "resourcemanager",
|
||||
Id: "id",
|
||||
Time: expectedTime,
|
||||
DataContentType: ContentTypeCloudEventsProtobuf,
|
||||
DataType: "type",
|
||||
Subject: "subject",
|
||||
}
|
||||
assert.NoError(t, send(&topicNameResolver, &messagingApi, context.Background(), RoutableSystemIdentifier, &cloudEvent))
|
||||
assert.True(t, messagingApiMock.AssertNumberOfCalls(t, "Send", 1))
|
||||
arguments := messagingApiMock.Calls[0].Arguments
|
||||
topic := arguments.Get(1).(string)
|
||||
assert.Equal(t, "topic", topic)
|
||||
contentType := arguments.Get(3).(string)
|
||||
assert.Equal(t, ContentTypeCloudEventsProtobuf, contentType)
|
||||
applicationProperties := arguments.Get(4).(map[string]any)
|
||||
assert.Equal(t, "1.0", applicationProperties["cloudEvents:specversion"])
|
||||
assert.Equal(t, "resourcemanager", applicationProperties["cloudEvents:source"])
|
||||
assert.Equal(t, "id", applicationProperties["cloudEvents:id"])
|
||||
assert.Equal(t, expectedTime.UnixMilli(), applicationProperties["cloudEvents:time"])
|
||||
assert.Equal(t, ContentTypeCloudEventsProtobuf, applicationProperties["cloudEvents:datacontenttype"])
|
||||
assert.Equal(t, "type", applicationProperties["cloudEvents:type"])
|
||||
assert.Equal(t, "subject", applicationProperties["cloudEvents:subject"])
|
||||
assert.Equal(t, nil, applicationProperties["cloudEvents:traceparent"])
|
||||
assert.Equal(t, nil, applicationProperties["cloudEvents:tracestate"])
|
||||
messagingApiMock.AssertExpectations(t)
|
||||
}
|
||||
164
audit/api/api_legacy.go
Normal file
164
audit/api/api_legacy.go
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/messaging"
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const DataTypeLegacyAuditEventV1 = "audit.v1.LegacyAuditEvent"
|
||||
|
||||
// LegacyTopicNameResolver implements TopicNameResolver.
|
||||
// A hard-coded topic name is used, routing identifiers are ignored.
|
||||
type LegacyTopicNameResolver struct {
|
||||
topicName string
|
||||
}
|
||||
|
||||
// Resolve implements TopicNameResolver.Resolve
|
||||
func (r *LegacyTopicNameResolver) Resolve(*RoutableIdentifier) (string, error) {
|
||||
return r.topicName, nil
|
||||
}
|
||||
|
||||
// LegacyTopicNameConfig provides topic name information required for the topic name resolution.
|
||||
type LegacyTopicNameConfig struct {
|
||||
TopicName string
|
||||
}
|
||||
|
||||
// LegacyAuditApi is an implementation of AuditApi to send events to the legacy audit log system.
|
||||
//
|
||||
// Note: The implementation will be deprecated and replaced with the "routableAuditApi" once the new audit log routing is implemented
|
||||
type LegacyAuditApi struct {
|
||||
messagingApi *messaging.Api
|
||||
topicNameResolver *TopicNameResolver
|
||||
validator *ProtobufValidator
|
||||
}
|
||||
|
||||
// NewLegacyAuditApi can be used to initialize the audit log api.
|
||||
//
|
||||
// Note: The NewLegacyAuditApi method will be deprecated and replaced with "newRoutableAuditApi" once the new audit log routing is implemented
|
||||
func NewLegacyAuditApi(
|
||||
messagingApi *messaging.Api,
|
||||
topicNameConfig LegacyTopicNameConfig,
|
||||
validator ProtobufValidator,
|
||||
) (*AuditApi, error) {
|
||||
|
||||
if messagingApi == nil {
|
||||
return nil, ErrMessagingApiNil
|
||||
}
|
||||
|
||||
// Topic resolver
|
||||
if topicNameConfig.TopicName == "" {
|
||||
return nil, errors.New("topic name is required")
|
||||
}
|
||||
var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: topicNameConfig.TopicName}
|
||||
|
||||
// Audit api
|
||||
var auditApi AuditApi = &LegacyAuditApi{
|
||||
messagingApi: messagingApi,
|
||||
topicNameResolver: &topicNameResolver,
|
||||
validator: &validator,
|
||||
}
|
||||
|
||||
return &auditApi, nil
|
||||
}
|
||||
|
||||
// Log implements AuditApi.Log
|
||||
func (a *LegacyAuditApi) Log(
|
||||
ctx context.Context,
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
) error {
|
||||
|
||||
return a.LogWithTrace(ctx, event, visibility, routableIdentifier, nil, nil)
|
||||
}
|
||||
|
||||
// LogWithTrace implements AuditApi.LogWithTrace
|
||||
func (a *LegacyAuditApi) LogWithTrace(
|
||||
ctx context.Context,
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
traceParent *string,
|
||||
traceState *string,
|
||||
) error {
|
||||
|
||||
cloudEvent, err := a.ValidateAndSerializeWithTrace(event, visibility, routableIdentifier, traceParent, traceState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.Send(ctx, routableIdentifier, cloudEvent)
|
||||
}
|
||||
|
||||
// ValidateAndSerialize implements AuditApi.ValidateAndSerialize.
|
||||
// It serializes the event into the byte representation of the legacy audit log system.
|
||||
func (a *LegacyAuditApi) ValidateAndSerialize(
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
) (*CloudEvent, error) {
|
||||
return a.ValidateAndSerializeWithTrace(event, visibility, routableIdentifier, nil, nil)
|
||||
}
|
||||
|
||||
// ValidateAndSerializeWithTrace implements AuditApi.ValidateAndSerializeWithTrace.
|
||||
// It serializes the event into the byte representation of the legacy audit log system.
|
||||
func (a *LegacyAuditApi) ValidateAndSerializeWithTrace(
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
traceParent *string,
|
||||
traceState *string,
|
||||
) (*CloudEvent, error) {
|
||||
|
||||
routableEvent, err := validateAndSerializePartially(a.validator, event, visibility, routableIdentifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reject event type data-access as the downstream services
|
||||
// cannot handle it at the moment
|
||||
if strings.HasSuffix(event.LogName, string(EventTypeDataAccess)) {
|
||||
return nil, ErrUnsupportedEventTypeDataAccess
|
||||
}
|
||||
|
||||
// Do nothing with the serialized data in the legacy solution
|
||||
_, err = proto.Marshal(routableEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert attributes
|
||||
legacyBytes, err := convertAndSerializeIntoLegacyFormat(event, routableEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
message := CloudEvent{
|
||||
SpecVersion: "1.0",
|
||||
Source: event.ProtoPayload.ServiceName,
|
||||
Id: event.InsertId,
|
||||
Time: event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(),
|
||||
DataContentType: ContentTypeCloudEventsJson,
|
||||
DataType: DataTypeLegacyAuditEventV1,
|
||||
Subject: event.ProtoPayload.ResourceName,
|
||||
Data: legacyBytes,
|
||||
TraceParent: traceParent,
|
||||
TraceState: traceState,
|
||||
}
|
||||
return &message, nil
|
||||
}
|
||||
|
||||
// Send implements AuditApi.Send
|
||||
func (a *LegacyAuditApi) Send(
|
||||
ctx context.Context,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
cloudEvent *CloudEvent,
|
||||
) error {
|
||||
|
||||
return send(a.topicNameResolver, a.messagingApi, ctx, routableIdentifier, cloudEvent)
|
||||
}
|
||||
312
audit/api/api_legacy_converter.go
Normal file
312
audit/api/api_legacy_converter.go
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
)
|
||||
|
||||
var ErrUnsupportedSeverity = errors.New("unsupported severity level")
|
||||
|
||||
// convertAndSerializeIntoLegacyFormat converts the protobuf events into the json serialized legacy audit log format
|
||||
func convertAndSerializeIntoLegacyFormat(
|
||||
event *auditV1.AuditLogEntry,
|
||||
routableEvent *auditV1.RoutableAuditEvent,
|
||||
) ([]byte, error) {
|
||||
|
||||
// Event type
|
||||
var eventType string
|
||||
if strings.HasSuffix(event.LogName, string(EventTypeAdminActivity)) {
|
||||
eventType = "ADMIN_ACTIVITY"
|
||||
} else if strings.HasSuffix(event.LogName, string(EventTypeSystemEvent)) {
|
||||
eventType = "SYSTEM_EVENT"
|
||||
} else if strings.HasSuffix(event.LogName, string(EventTypePolicyDenied)) {
|
||||
eventType = "POLICY_DENIED"
|
||||
} else if strings.HasSuffix(event.LogName, string(EventTypeDataAccess)) {
|
||||
return nil, ErrUnsupportedEventTypeDataAccess
|
||||
} else {
|
||||
return nil, errors.New("unsupported event type")
|
||||
}
|
||||
|
||||
// Source IP & User agent
|
||||
var sourceIpAddress string
|
||||
var userAgent string
|
||||
if event.ProtoPayload == nil || event.ProtoPayload.RequestMetadata == nil {
|
||||
sourceIpAddress = "0.0.0.0"
|
||||
userAgent = "none"
|
||||
} else {
|
||||
sourceIpAddress = event.ProtoPayload.RequestMetadata.CallerIp
|
||||
userAgent = event.ProtoPayload.RequestMetadata.CallerSuppliedUserAgent
|
||||
}
|
||||
|
||||
// Principals
|
||||
var serviceAccountDelegationInfo *LegacyAuditEventServiceAccountDelegationInfo = nil
|
||||
if len(event.ProtoPayload.AuthenticationInfo.ServiceAccountDelegationInfo) > 0 {
|
||||
var principals []LegacyAuditEventPrincipal
|
||||
for _, principal := range event.ProtoPayload.AuthenticationInfo.ServiceAccountDelegationInfo {
|
||||
switch principalValue := principal.Authority.(type) {
|
||||
case *auditV1.ServiceAccountDelegationInfo_IdpPrincipal_:
|
||||
principals = append(principals, LegacyAuditEventPrincipal{
|
||||
Id: principalValue.IdpPrincipal.PrincipalId,
|
||||
Email: &principalValue.IdpPrincipal.PrincipalEmail,
|
||||
})
|
||||
case *auditV1.ServiceAccountDelegationInfo_SystemPrincipal_:
|
||||
principals = append(principals, LegacyAuditEventPrincipal{
|
||||
Id: "system",
|
||||
})
|
||||
default:
|
||||
return nil, errors.New("unsupported principal type")
|
||||
}
|
||||
}
|
||||
serviceAccountDelegationInfo = &LegacyAuditEventServiceAccountDelegationInfo{Principals: principals}
|
||||
}
|
||||
|
||||
var request LegacyAuditEventRequest
|
||||
if event.ProtoPayload.RequestMetadata.RequestAttributes == nil {
|
||||
request = LegacyAuditEventRequest{
|
||||
Endpoint: "none",
|
||||
}
|
||||
} else {
|
||||
var parameters map[string]interface{} = nil
|
||||
if event.ProtoPayload.RequestMetadata.RequestAttributes.Path != "" &&
|
||||
event.ProtoPayload.RequestMetadata.RequestAttributes.Query != nil &&
|
||||
*event.ProtoPayload.RequestMetadata.RequestAttributes.Query != "" {
|
||||
parameters = map[string]interface{}{}
|
||||
|
||||
unescapedQuery, err := url.QueryUnescape(*event.ProtoPayload.RequestMetadata.RequestAttributes.Query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parsedUrl, err := url.Parse(fmt.Sprintf("%s?%s",
|
||||
event.ProtoPayload.RequestMetadata.RequestAttributes.Path,
|
||||
unescapedQuery))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for k, v := range parsedUrl.Query() {
|
||||
parameters[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
var body map[string]interface{} = nil
|
||||
if event.ProtoPayload.Request != nil {
|
||||
body = event.ProtoPayload.Request.AsMap()
|
||||
}
|
||||
var headers map[string]interface{} = nil
|
||||
if event.ProtoPayload.RequestMetadata.RequestAttributes.Headers != nil {
|
||||
headers = map[string]interface{}{}
|
||||
for key, value := range event.ProtoPayload.RequestMetadata.RequestAttributes.Headers {
|
||||
headers[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
request = LegacyAuditEventRequest{
|
||||
Endpoint: event.ProtoPayload.RequestMetadata.RequestAttributes.Path,
|
||||
Parameters: ¶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"`
|
||||
}
|
||||
21
audit/api/api_legacy_converter_test.go
Normal file
21
audit/api/api_legacy_converter_test.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_ConvertAndSerializeIntoLegacyFormat_NoObjectIdentifier(t *testing.T) {
|
||||
event, _ := newProjectAuditEvent(nil)
|
||||
routableEvent := auditV1.RoutableAuditEvent{
|
||||
OperationName: event.ProtoPayload.OperationName,
|
||||
Visibility: auditV1.Visibility_VISIBILITY_PUBLIC,
|
||||
ObjectIdentifier: nil,
|
||||
Data: nil,
|
||||
}
|
||||
|
||||
_, err := convertAndSerializeIntoLegacyFormat(event, &routableEvent)
|
||||
assert.ErrorIs(t, err, ErrObjectIdentifierNil)
|
||||
}
|
||||
160
audit/api/api_legacy_dynamic.go
Normal file
160
audit/api/api_legacy_dynamic.go
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/messaging"
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type ContextKey string
|
||||
|
||||
const ContextKeyTopic ContextKey = "topic"
|
||||
|
||||
var ErrNoTopicNameProvided = errors.New("no topic name provided")
|
||||
var ErrTopicNameEmpty = errors.New("empty topic name provided")
|
||||
|
||||
// DynamicLegacyAuditApi is an implementation of AuditApi to send events to the legacy audit log system
|
||||
// by setting the topic name explicitly in the context with the key "topic".
|
||||
//
|
||||
// Note: The implementation will be deprecated and replaced with the "routableAuditApi" once the new audit log routing is implemented
|
||||
type DynamicLegacyAuditApi struct {
|
||||
messagingApi *messaging.Api
|
||||
validator *ProtobufValidator
|
||||
}
|
||||
|
||||
// NewDynamicLegacyAuditApi can be used to initialize the audit log api.
|
||||
//
|
||||
// Note: The NewLegacyAuditApi method will be deprecated and replaced with "newRoutableAuditApi" once the new audit log routing is implemented
|
||||
func NewDynamicLegacyAuditApi(
|
||||
messagingApi *messaging.Api,
|
||||
validator ProtobufValidator,
|
||||
) (*AuditApi, error) {
|
||||
|
||||
if messagingApi == nil {
|
||||
return nil, ErrMessagingApiNil
|
||||
}
|
||||
|
||||
// Audit api
|
||||
var auditApi AuditApi = &DynamicLegacyAuditApi{
|
||||
messagingApi: messagingApi,
|
||||
validator: &validator,
|
||||
}
|
||||
|
||||
return &auditApi, nil
|
||||
}
|
||||
|
||||
// Log implements AuditApi.Log
|
||||
func (a *DynamicLegacyAuditApi) Log(
|
||||
ctx context.Context,
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
) error {
|
||||
|
||||
return a.LogWithTrace(ctx, event, visibility, routableIdentifier, nil, nil)
|
||||
}
|
||||
|
||||
// LogWithTrace implements AuditApi.LogWithTrace
|
||||
func (a *DynamicLegacyAuditApi) LogWithTrace(
|
||||
ctx context.Context,
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
traceParent *string,
|
||||
traceState *string,
|
||||
) error {
|
||||
|
||||
cloudEvent, err := a.ValidateAndSerializeWithTrace(event, visibility, routableIdentifier, traceParent, traceState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.Send(ctx, routableIdentifier, cloudEvent)
|
||||
}
|
||||
|
||||
// ValidateAndSerialize implements AuditApi.ValidateAndSerialize.
|
||||
// It serializes the event into the byte representation of the legacy audit log system.
|
||||
func (a *DynamicLegacyAuditApi) ValidateAndSerialize(
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
) (*CloudEvent, error) {
|
||||
return a.ValidateAndSerializeWithTrace(event, visibility, routableIdentifier, nil, nil)
|
||||
}
|
||||
|
||||
// ValidateAndSerializeWithTrace implements AuditApi.ValidateAndSerializeWithTrace.
|
||||
// It serializes the event into the byte representation of the legacy audit log system.
|
||||
func (a *DynamicLegacyAuditApi) ValidateAndSerializeWithTrace(
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
traceParent *string,
|
||||
traceState *string,
|
||||
) (*CloudEvent, error) {
|
||||
|
||||
routableEvent, err := validateAndSerializePartially(a.validator, event, visibility, routableIdentifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reject event type data-access as the downstream services
|
||||
// cannot handle it at the moment
|
||||
if strings.HasSuffix(event.LogName, string(EventTypeDataAccess)) {
|
||||
return nil, ErrUnsupportedEventTypeDataAccess
|
||||
}
|
||||
|
||||
// Do nothing with the serialized data in the legacy solution
|
||||
_, err = proto.Marshal(routableEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert attributes
|
||||
legacyBytes, err := convertAndSerializeIntoLegacyFormat(event, routableEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
message := CloudEvent{
|
||||
SpecVersion: "1.0",
|
||||
Source: event.ProtoPayload.ServiceName,
|
||||
Id: event.InsertId,
|
||||
Time: event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(),
|
||||
DataContentType: ContentTypeCloudEventsJson,
|
||||
DataType: DataTypeLegacyAuditEventV1,
|
||||
Subject: event.ProtoPayload.ResourceName,
|
||||
Data: legacyBytes,
|
||||
TraceParent: traceParent,
|
||||
TraceState: traceState,
|
||||
}
|
||||
return &message, nil
|
||||
}
|
||||
|
||||
// Send implements AuditApi.Send
|
||||
//
|
||||
// Requires to have the topic name set as key "topic" in the context.
|
||||
func (a *DynamicLegacyAuditApi) Send(
|
||||
ctx context.Context,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
cloudEvent *CloudEvent,
|
||||
) error {
|
||||
|
||||
rawTopicName := ctx.Value(ContextKeyTopic)
|
||||
if rawTopicName == nil {
|
||||
return ErrNoTopicNameProvided
|
||||
}
|
||||
topicName := fmt.Sprintf("%s", rawTopicName)
|
||||
if len(topicName) == 0 {
|
||||
return ErrTopicNameEmpty
|
||||
}
|
||||
|
||||
var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: topicName}
|
||||
|
||||
return send(&topicNameResolver, a.messagingApi, ctx, routableIdentifier, cloudEvent)
|
||||
}
|
||||
528
audit/api/api_legacy_dynamic_test.go
Normal file
528
audit/api/api_legacy_dynamic_test.go
Normal file
|
|
@ -0,0 +1,528 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/messaging"
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||
|
||||
"github.com/bufbuild/protovalidate-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func TestDynamicLegacyAuditApi(t *testing.T) {
|
||||
|
||||
// Specify test timeout
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancelFn()
|
||||
|
||||
// Start solace docker container
|
||||
solaceContainer, err := messaging.NewSolaceContainer(context.Background())
|
||||
assert.NoError(t, err)
|
||||
defer solaceContainer.Stop()
|
||||
|
||||
// Instantiate the messaging api
|
||||
messagingApi, err := messaging.NewAmqpApi(messaging.AmqpConfig{URL: solaceContainer.AmqpConnectionString})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Validator
|
||||
validator, err := protovalidate.New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
topicSubscriptionTopicPattern := "audit-log/>"
|
||||
traceParent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
|
||||
traceState := "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"
|
||||
|
||||
// Check that event-type data-access is rejected as it is currently
|
||||
// not supported by downstream services
|
||||
t.Run("reject data access event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "reject-data-access-legacy"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/organization-rejected"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewDynamicLegacyAuditApi(
|
||||
messagingApi,
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newOrganizationAuditEvent(nil)
|
||||
event.LogName = strings.Replace(event.LogName, string(EventTypeAdminActivity), string(EventTypeDataAccess), 1)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
ctx := context.WithValue(ctx, ContextKeyTopic, topicName)
|
||||
assert.ErrorIs(t, (*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
), ErrUnsupportedEventTypeDataAccess)
|
||||
})
|
||||
|
||||
// Check logging of organization events
|
||||
t.Run("Log public organization event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "org-event-public-legacy"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/organization-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewDynamicLegacyAuditApi(
|
||||
messagingApi,
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newOrganizationAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
ctx := context.WithValue(ctx, ContextKeyTopic, topicName)
|
||||
assert.NoError(t, (*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentMessage(t, topicName, message, event, &traceParent, &traceState)
|
||||
})
|
||||
|
||||
t.Run("Log private organization event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "org-event-private-legacy"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/organization-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewDynamicLegacyAuditApi(
|
||||
messagingApi,
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newOrganizationAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
ctx := context.WithValue(ctx, ContextKeyTopic, topicName)
|
||||
assert.NoError(t, (*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentMessage(t, topicName, message, event, &traceParent, &traceState)
|
||||
})
|
||||
|
||||
// Check logging of folder events
|
||||
t.Run("Log public folder event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "folder-event-public-legacy"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/folder-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewDynamicLegacyAuditApi(
|
||||
messagingApi,
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newFolderAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
ctx := context.WithValue(ctx, ContextKeyTopic, topicName)
|
||||
assert.NoError(t, (*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentMessage(t, topicName, message, event, &traceParent, &traceState)
|
||||
})
|
||||
|
||||
t.Run("Log private folder event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "folder-event-private-legacy"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/folder-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewDynamicLegacyAuditApi(
|
||||
messagingApi,
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newFolderAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
ctx := context.WithValue(ctx, ContextKeyTopic, topicName)
|
||||
assert.NoError(t, (*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentMessage(t, topicName, message, event, &traceParent, &traceState)
|
||||
})
|
||||
|
||||
// Check logging of project events
|
||||
t.Run("Log public project event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "project-event-public-legacy"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/project-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewDynamicLegacyAuditApi(
|
||||
messagingApi,
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newProjectAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
ctx := context.WithValue(ctx, ContextKeyTopic, topicName)
|
||||
assert.NoError(t, (*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentMessage(t, topicName, message, event, &traceParent, &traceState)
|
||||
})
|
||||
|
||||
t.Run("Log private project event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "project-event-private-legacy"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/project-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewDynamicLegacyAuditApi(
|
||||
messagingApi,
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newProjectAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
ctx := context.WithValue(ctx, ContextKeyTopic, topicName)
|
||||
assert.NoError(t, (*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentMessage(t, topicName, message, event, &traceParent, &traceState)
|
||||
})
|
||||
|
||||
// Check logging of system events with identifier
|
||||
t.Run("Log private project system event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
queueName := "project-system-event-private"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/project-system-changed"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewDynamicLegacyAuditApi(
|
||||
messagingApi,
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event := newProjectSystemAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
ctx := context.WithValue(ctx, ContextKeyTopic, topicName)
|
||||
assert.NoError(t,
|
||||
(*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
RoutableSystemIdentifier,
|
||||
nil,
|
||||
nil,
|
||||
))
|
||||
|
||||
// Receive the event from solace
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check topic name
|
||||
assert.Equal(t, topicName, *message.Properties.To)
|
||||
assert.Nil(t, message.ApplicationProperties["cloudEvents:traceparent"])
|
||||
assert.Nil(t, message.ApplicationProperties["cloudEvents:tracestate"])
|
||||
|
||||
// Check deserialized message
|
||||
var auditEvent LegacyAuditEvent
|
||||
assert.NoError(t, json.Unmarshal(message.Data[0], &auditEvent))
|
||||
|
||||
assert.Equal(t, event.ProtoPayload.ResourceName, *auditEvent.ResourceName)
|
||||
assert.Equal(t, event.ProtoPayload.OperationName, auditEvent.EventName)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(), auditEvent.EventTimeStamp)
|
||||
assert.Equal(t, event.ProtoPayload.AuthenticationInfo.PrincipalId, auditEvent.Initiator.Id)
|
||||
assert.Equal(t, "SYSTEM_EVENT", auditEvent.EventType)
|
||||
assert.Equal(t, "INFO", auditEvent.Severity)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Path, auditEvent.Request.Endpoint)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.CallerIp, auditEvent.SourceIpAddress)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.CallerSuppliedUserAgent, auditEvent.UserAgent)
|
||||
})
|
||||
|
||||
// Check logging of system events
|
||||
t.Run("Log private system event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
queueName := "system-event-private"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/system-changed"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewDynamicLegacyAuditApi(
|
||||
messagingApi,
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event := newSystemAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
ctx := context.WithValue(ctx, ContextKeyTopic, topicName)
|
||||
assert.NoError(t,
|
||||
(*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
RoutableSystemIdentifier,
|
||||
nil,
|
||||
nil,
|
||||
))
|
||||
|
||||
// Receive the event from solace
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check topic name
|
||||
assert.Equal(t, topicName, *message.Properties.To)
|
||||
assert.Nil(t, message.ApplicationProperties["cloudEvents:traceparent"])
|
||||
assert.Nil(t, message.ApplicationProperties["cloudEvents:tracestate"])
|
||||
|
||||
// Check deserialized message
|
||||
var auditEvent LegacyAuditEvent
|
||||
assert.NoError(t, json.Unmarshal(message.Data[0], &auditEvent))
|
||||
|
||||
assert.Equal(t, event.ProtoPayload.OperationName, auditEvent.EventName)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(), auditEvent.EventTimeStamp)
|
||||
assert.Equal(t, event.ProtoPayload.AuthenticationInfo.PrincipalId, auditEvent.Initiator.Id)
|
||||
assert.Equal(t, "SYSTEM_EVENT", auditEvent.EventType)
|
||||
assert.Equal(t, "INFO", auditEvent.Severity)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Path, auditEvent.Request.Endpoint)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.CallerIp, auditEvent.SourceIpAddress)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.CallerSuppliedUserAgent, auditEvent.UserAgent)
|
||||
})
|
||||
|
||||
t.Run("Log event with details", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "org-event-with-details-legacy"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/organization-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewDynamicLegacyAuditApi(
|
||||
messagingApi,
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newOrganizationAuditEvent(nil)
|
||||
escapedQuery := url.QueryEscape("param=value")
|
||||
event.ProtoPayload.RequestMetadata.RequestAttributes.Query = &escapedQuery
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
ctx := context.WithValue(ctx, ContextKeyTopic, topicName)
|
||||
assert.NoError(t, (*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentMessageWithDetails(t, topicName, message, event, &traceParent, &traceState)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDynamicLegacyAuditApi_NewLegacyAuditApi_MessagingApiNil(t *testing.T) {
|
||||
auditApi, err := NewDynamicLegacyAuditApi(nil, nil)
|
||||
assert.Nil(t, auditApi)
|
||||
assert.EqualError(t, err, "messaging api nil")
|
||||
}
|
||||
|
||||
func TestDynamicLegacyAuditApi_ValidateAndSerialize_ValidationFailed(t *testing.T) {
|
||||
expectedError := errors.New("expected error")
|
||||
|
||||
validator := &ProtobufValidatorMock{}
|
||||
validator.On("Validate", mock.Anything).Return(expectedError)
|
||||
var protobufValidator ProtobufValidator = validator
|
||||
|
||||
auditApi := DynamicLegacyAuditApi{validator: &protobufValidator}
|
||||
|
||||
event := newSystemAuditEvent(nil)
|
||||
_, err := auditApi.ValidateAndSerialize(event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
|
||||
assert.ErrorIs(t, err, expectedError)
|
||||
}
|
||||
|
||||
func TestDynamicLegacyAuditApi_Log_ValidationFailed(t *testing.T) {
|
||||
expectedError := errors.New("expected error")
|
||||
|
||||
validator := &ProtobufValidatorMock{}
|
||||
validator.On("Validate", mock.Anything).Return(expectedError)
|
||||
var protobufValidator ProtobufValidator = validator
|
||||
|
||||
auditApi := DynamicLegacyAuditApi{validator: &protobufValidator}
|
||||
|
||||
event := newSystemAuditEvent(nil)
|
||||
err := auditApi.Log(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
|
||||
assert.ErrorIs(t, err, expectedError)
|
||||
}
|
||||
|
||||
func TestDynamicLegacyAuditApi_Log_NilEvent(t *testing.T) {
|
||||
auditApi := DynamicLegacyAuditApi{}
|
||||
err := auditApi.Log(context.Background(), nil, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
|
||||
assert.ErrorIs(t, err, ErrEventNil)
|
||||
}
|
||||
|
||||
func TestDynamicLegacyAuditApi_ConvertAndSerializeIntoLegacyFormatInvalidObjectIdentifierType(t *testing.T) {
|
||||
customization := func(event *auditV1.AuditLogEntry,
|
||||
objectIdentifier *auditV1.ObjectIdentifier) {
|
||||
objectIdentifier.Type = "invalid"
|
||||
}
|
||||
event, objectIdentifier := newProjectAuditEvent(&customization)
|
||||
|
||||
validator := &ProtobufValidatorMock{}
|
||||
validator.On("Validate", mock.Anything).Return(nil)
|
||||
var protobufValidator ProtobufValidator = validator
|
||||
|
||||
auditApi := DynamicLegacyAuditApi{validator: &protobufValidator}
|
||||
_, err := auditApi.ValidateAndSerialize(event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier))
|
||||
assert.ErrorIs(t, err, ErrUnsupportedRoutableType)
|
||||
}
|
||||
671
audit/api/api_legacy_test.go
Normal file
671
audit/api/api_legacy_test.go
Normal file
|
|
@ -0,0 +1,671 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/messaging"
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||
|
||||
"github.com/Azure/go-amqp"
|
||||
"github.com/bufbuild/protovalidate-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func TestLegacyAuditApi(t *testing.T) {
|
||||
|
||||
// Specify test timeout
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancelFn()
|
||||
|
||||
// Start solace docker container
|
||||
solaceContainer, err := messaging.NewSolaceContainer(context.Background())
|
||||
assert.NoError(t, err)
|
||||
defer solaceContainer.Stop()
|
||||
|
||||
// Instantiate the messaging api
|
||||
messagingApi, err := messaging.NewAmqpApi(messaging.AmqpConfig{URL: solaceContainer.AmqpConnectionString})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Validator
|
||||
validator, err := protovalidate.New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
topicSubscriptionTopicPattern := "audit-log/>"
|
||||
traceParent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
|
||||
traceState := "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"
|
||||
|
||||
// Check that event-type data-access is rejected as it is currently
|
||||
// not supported by downstream services
|
||||
t.Run("reject data access event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "org-reject-data-access-legacy"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/organization-rejected"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewLegacyAuditApi(
|
||||
messagingApi,
|
||||
LegacyTopicNameConfig{TopicName: topicName},
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newOrganizationAuditEvent(nil)
|
||||
event.LogName = strings.Replace(event.LogName, string(EventTypeAdminActivity), string(EventTypeDataAccess), 1)
|
||||
|
||||
// Log the event to solace
|
||||
assert.ErrorIs(t, (*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
auditV1.Visibility_VISIBILITY_PUBLIC,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
), ErrUnsupportedEventTypeDataAccess)
|
||||
})
|
||||
|
||||
// Check logging of organization events
|
||||
t.Run("Log public organization event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "org-event-public-legacy"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/organization-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewLegacyAuditApi(
|
||||
messagingApi,
|
||||
LegacyTopicNameConfig{TopicName: topicName},
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newOrganizationAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
assert.NoError(t, (*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentMessage(t, topicName, message, event, &traceParent, &traceState)
|
||||
})
|
||||
|
||||
t.Run("Log private organization event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "org-event-private-legacy"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/organization-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewLegacyAuditApi(
|
||||
messagingApi,
|
||||
LegacyTopicNameConfig{TopicName: topicName},
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newOrganizationAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
assert.NoError(t, (*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentMessage(t, topicName, message, event, &traceParent, &traceState)
|
||||
})
|
||||
|
||||
// Check logging of folder events
|
||||
t.Run("Log public folder event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "folder-event-public-legacy"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/folder-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewLegacyAuditApi(
|
||||
messagingApi,
|
||||
LegacyTopicNameConfig{TopicName: topicName},
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newFolderAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
assert.NoError(t, (*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentMessage(t, topicName, message, event, &traceParent, &traceState)
|
||||
})
|
||||
|
||||
t.Run("Log private folder event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "folder-event-private-legacy"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/folder-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewLegacyAuditApi(
|
||||
messagingApi,
|
||||
LegacyTopicNameConfig{TopicName: topicName},
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newFolderAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
assert.NoError(t, (*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentMessage(t, topicName, message, event, &traceParent, &traceState)
|
||||
})
|
||||
|
||||
// Check logging of project events
|
||||
t.Run("Log public project event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "project-event-public-legacy"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/project-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewLegacyAuditApi(
|
||||
messagingApi,
|
||||
LegacyTopicNameConfig{TopicName: topicName},
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newProjectAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
assert.NoError(t, (*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentMessage(t, topicName, message, event, &traceParent, &traceState)
|
||||
})
|
||||
|
||||
t.Run("Log private project event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "project-event-private-legacy"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/project-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewLegacyAuditApi(
|
||||
messagingApi,
|
||||
LegacyTopicNameConfig{TopicName: topicName},
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newProjectAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
assert.NoError(t, (*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentMessage(t, topicName, message, event, &traceParent, &traceState)
|
||||
})
|
||||
|
||||
// Check logging of system events with identifier
|
||||
t.Run("Log private project system event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
queueName := "project-system-event-private"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/project-system-changed"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewLegacyAuditApi(
|
||||
messagingApi,
|
||||
LegacyTopicNameConfig{TopicName: topicName},
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event := newProjectSystemAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
assert.NoError(t,
|
||||
(*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
RoutableSystemIdentifier,
|
||||
nil,
|
||||
nil,
|
||||
))
|
||||
|
||||
// Receive the event from solace
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check topic name
|
||||
assert.Equal(t, topicName, *message.Properties.To)
|
||||
assert.Nil(t, message.ApplicationProperties["cloudEvents:traceparent"])
|
||||
assert.Nil(t, message.ApplicationProperties["cloudEvents:tracestate"])
|
||||
|
||||
// Check deserialized message
|
||||
var auditEvent LegacyAuditEvent
|
||||
assert.NoError(t, json.Unmarshal(message.Data[0], &auditEvent))
|
||||
|
||||
assert.Equal(t, event.ProtoPayload.ResourceName, *auditEvent.ResourceName)
|
||||
assert.Equal(t, event.ProtoPayload.OperationName, auditEvent.EventName)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(), auditEvent.EventTimeStamp)
|
||||
assert.Equal(t, event.ProtoPayload.AuthenticationInfo.PrincipalId, auditEvent.Initiator.Id)
|
||||
assert.Equal(t, "SYSTEM_EVENT", auditEvent.EventType)
|
||||
assert.Equal(t, "INFO", auditEvent.Severity)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Path, auditEvent.Request.Endpoint)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.CallerIp, auditEvent.SourceIpAddress)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.CallerSuppliedUserAgent, auditEvent.UserAgent)
|
||||
})
|
||||
|
||||
// Check logging of system events
|
||||
t.Run("Log private system event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
queueName := "system-event-private"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/system-changed"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewLegacyAuditApi(
|
||||
messagingApi,
|
||||
LegacyTopicNameConfig{TopicName: topicName},
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event := newSystemAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
assert.NoError(t,
|
||||
(*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
RoutableSystemIdentifier,
|
||||
nil,
|
||||
nil,
|
||||
))
|
||||
|
||||
// Receive the event from solace
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check topic name
|
||||
assert.Equal(t, topicName, *message.Properties.To)
|
||||
assert.Nil(t, message.ApplicationProperties["cloudEvents:traceparent"])
|
||||
assert.Nil(t, message.ApplicationProperties["cloudEvents:tracestate"])
|
||||
|
||||
// Check deserialized message
|
||||
var auditEvent LegacyAuditEvent
|
||||
assert.NoError(t, json.Unmarshal(message.Data[0], &auditEvent))
|
||||
|
||||
assert.Equal(t, event.ProtoPayload.OperationName, auditEvent.EventName)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(), auditEvent.EventTimeStamp)
|
||||
assert.Equal(t, event.ProtoPayload.AuthenticationInfo.PrincipalId, auditEvent.Initiator.Id)
|
||||
assert.Equal(t, "SYSTEM_EVENT", auditEvent.EventType)
|
||||
assert.Equal(t, "INFO", auditEvent.Severity)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Path, auditEvent.Request.Endpoint)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.CallerIp, auditEvent.SourceIpAddress)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.CallerSuppliedUserAgent, auditEvent.UserAgent)
|
||||
})
|
||||
|
||||
t.Run("Log event with details", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "org-event-with-details-legacy"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/organization-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewLegacyAuditApi(
|
||||
messagingApi,
|
||||
LegacyTopicNameConfig{TopicName: topicName},
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newOrganizationAuditEvent(nil)
|
||||
escapedQuery := url.QueryEscape("param=value")
|
||||
event.ProtoPayload.RequestMetadata.RequestAttributes.Query = &escapedQuery
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
assert.NoError(t, (*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentMessageWithDetails(t, topicName, message, event, &traceParent, &traceState)
|
||||
})
|
||||
}
|
||||
|
||||
func validateSentMessage(
|
||||
t *testing.T,
|
||||
topicName string,
|
||||
message *amqp.Message,
|
||||
event *auditV1.AuditLogEntry,
|
||||
traceParent *string,
|
||||
traceState *string,
|
||||
) {
|
||||
|
||||
// Check message properties
|
||||
assert.Equal(t, topicName, *message.Properties.To)
|
||||
assert.Equal(t, *traceParent, message.ApplicationProperties["cloudEvents:traceparent"])
|
||||
assert.Equal(t, *traceState, message.ApplicationProperties["cloudEvents:tracestate"])
|
||||
|
||||
// Check deserialized message
|
||||
var auditEvent LegacyAuditEvent
|
||||
assert.NoError(t, json.Unmarshal(message.Data[0], &auditEvent))
|
||||
|
||||
var severity string
|
||||
switch event.Severity {
|
||||
case auditV1.LogSeverity_LOG_SEVERITY_DEFAULT:
|
||||
fallthrough
|
||||
case auditV1.LogSeverity_LOG_SEVERITY_DEBUG:
|
||||
fallthrough
|
||||
case auditV1.LogSeverity_LOG_SEVERITY_INFO:
|
||||
fallthrough
|
||||
case auditV1.LogSeverity_LOG_SEVERITY_NOTICE:
|
||||
fallthrough
|
||||
case auditV1.LogSeverity_LOG_SEVERITY_WARNING:
|
||||
severity = "INFO"
|
||||
case auditV1.LogSeverity_LOG_SEVERITY_ERROR:
|
||||
fallthrough
|
||||
case auditV1.LogSeverity_LOG_SEVERITY_CRITICAL:
|
||||
fallthrough
|
||||
case auditV1.LogSeverity_LOG_SEVERITY_ALERT:
|
||||
fallthrough
|
||||
case auditV1.LogSeverity_LOG_SEVERITY_EMERGENCY:
|
||||
severity = "ERROR"
|
||||
default:
|
||||
assert.Fail(t, "unknown log severity")
|
||||
}
|
||||
|
||||
assert.Equal(t, event.ProtoPayload.OperationName, auditEvent.EventName)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(), auditEvent.EventTimeStamp)
|
||||
assert.Equal(t, event.ProtoPayload.AuthenticationInfo.PrincipalId, auditEvent.Initiator.Id)
|
||||
assert.Equal(t, "ADMIN_ACTIVITY", auditEvent.EventType)
|
||||
assert.Equal(t, severity, auditEvent.Severity)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Path, auditEvent.Request.Endpoint)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.CallerIp, auditEvent.SourceIpAddress)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.CallerSuppliedUserAgent, auditEvent.UserAgent)
|
||||
}
|
||||
|
||||
func validateSentMessageWithDetails(
|
||||
t *testing.T,
|
||||
topicName string,
|
||||
message *amqp.Message,
|
||||
event *auditV1.AuditLogEntry,
|
||||
traceParent *string,
|
||||
traceState *string,
|
||||
) {
|
||||
|
||||
// Check topic name
|
||||
assert.Equal(t, topicName, *message.Properties.To)
|
||||
assert.Equal(t, ContentTypeCloudEventsJson, message.ApplicationProperties["cloudEvents:datacontenttype"])
|
||||
assert.Equal(t, DataTypeLegacyAuditEventV1, message.ApplicationProperties["cloudEvents:type"])
|
||||
assert.Equal(t, *traceParent, message.ApplicationProperties["cloudEvents:traceparent"])
|
||||
assert.Equal(t, *traceState, message.ApplicationProperties["cloudEvents:tracestate"])
|
||||
|
||||
// Check deserialized message
|
||||
var auditEvent LegacyAuditEvent
|
||||
assert.NoError(t, json.Unmarshal(message.Data[0], &auditEvent))
|
||||
|
||||
assert.Equal(t, event.ProtoPayload.OperationName, auditEvent.EventName)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(), auditEvent.EventTimeStamp)
|
||||
assert.Equal(t, event.ProtoPayload.AuthenticationInfo.PrincipalId, auditEvent.Initiator.Id)
|
||||
assert.Equal(t, "ADMIN_ACTIVITY", auditEvent.EventType)
|
||||
assert.Equal(t, "INFO", auditEvent.Severity)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Path, auditEvent.Request.Endpoint)
|
||||
var parameters map[string]interface{} = nil
|
||||
if event.ProtoPayload.RequestMetadata.RequestAttributes.Path != "" &&
|
||||
event.ProtoPayload.RequestMetadata.RequestAttributes.Query != nil &&
|
||||
*event.ProtoPayload.RequestMetadata.RequestAttributes.Query != "" {
|
||||
parameters = map[string]interface{}{}
|
||||
|
||||
unescapedQuery, err := url.QueryUnescape(*event.ProtoPayload.RequestMetadata.RequestAttributes.Query)
|
||||
assert.NoError(t, err)
|
||||
parsedUrl, err := url.Parse(fmt.Sprintf("%s?%s",
|
||||
event.ProtoPayload.RequestMetadata.RequestAttributes.Path,
|
||||
unescapedQuery))
|
||||
|
||||
assert.NoError(t, err)
|
||||
for k, v := range parsedUrl.Query() {
|
||||
parameters[k] = v
|
||||
}
|
||||
}
|
||||
assert.Equal(t, event.ProtoPayload.Request.AsMap(), *auditEvent.Request.Body)
|
||||
assert.Equal(t, parameters, *auditEvent.Request.Parameters)
|
||||
for key, value := range event.ProtoPayload.RequestMetadata.RequestAttributes.Headers {
|
||||
assert.Equal(t, value, (*auditEvent.Request.Headers)[key])
|
||||
}
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.CallerIp, auditEvent.SourceIpAddress)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.CallerSuppliedUserAgent, auditEvent.UserAgent)
|
||||
assert.Equal(t, event.ProtoPayload.Request.AsMap(), *auditEvent.Details)
|
||||
assert.Equal(t, event.ProtoPayload.Response.AsMap(), *auditEvent.Result)
|
||||
assert.True(t, auditEvent.Context.OrganizationId != nil || auditEvent.Context.FolderId != nil || auditEvent.Context.ProjectId != nil)
|
||||
|
||||
for idx, principal := range event.ProtoPayload.AuthenticationInfo.ServiceAccountDelegationInfo {
|
||||
switch principalValue := principal.Authority.(type) {
|
||||
case *auditV1.ServiceAccountDelegationInfo_IdpPrincipal_:
|
||||
assert.Equal(t, principalValue.IdpPrincipal.PrincipalId, auditEvent.ServiceAccountDelegationInfo.Principals[idx].Id)
|
||||
assert.Equal(t, principalValue.IdpPrincipal.PrincipalEmail, auditEvent.ServiceAccountDelegationInfo.Principals[idx].Email)
|
||||
case *auditV1.ServiceAccountDelegationInfo_SystemPrincipal_:
|
||||
assert.Equal(t, "system", auditEvent.ServiceAccountDelegationInfo.Principals[idx].Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyAuditApi_NewLegacyAuditApi(t *testing.T) {
|
||||
|
||||
t.Run("messaging api nil", func(t *testing.T) {
|
||||
auditApi, err := NewLegacyAuditApi(nil, LegacyTopicNameConfig{}, nil)
|
||||
assert.Nil(t, auditApi)
|
||||
assert.EqualError(t, err, "messaging api nil")
|
||||
})
|
||||
|
||||
t.Run("topic name is blank", func(t *testing.T) {
|
||||
// Start solace docker container
|
||||
solaceContainer, err := messaging.NewSolaceContainer(context.Background())
|
||||
assert.NoError(t, err)
|
||||
defer solaceContainer.Stop()
|
||||
|
||||
// Instantiate the messaging api
|
||||
messagingApi, err := messaging.NewAmqpApi(messaging.AmqpConfig{URL: solaceContainer.AmqpConnectionString})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Validator
|
||||
validator, err := protovalidate.New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
auditApi, err := NewLegacyAuditApi(messagingApi, LegacyTopicNameConfig{
|
||||
TopicName: "",
|
||||
}, validator)
|
||||
|
||||
assert.Nil(t, auditApi)
|
||||
assert.EqualError(t, err, "topic name is required")
|
||||
})
|
||||
}
|
||||
|
||||
func TestLegacyAuditApi_ValidateAndSerialize_ValidationFailed(t *testing.T) {
|
||||
expectedError := errors.New("expected error")
|
||||
|
||||
validator := &ProtobufValidatorMock{}
|
||||
validator.On("Validate", mock.Anything).Return(expectedError)
|
||||
var protobufValidator ProtobufValidator = validator
|
||||
|
||||
auditApi := LegacyAuditApi{validator: &protobufValidator}
|
||||
|
||||
event := newSystemAuditEvent(nil)
|
||||
_, err := auditApi.ValidateAndSerialize(event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
|
||||
assert.ErrorIs(t, err, expectedError)
|
||||
}
|
||||
|
||||
func TestLegacyAuditApi_Log_ValidationFailed(t *testing.T) {
|
||||
expectedError := errors.New("expected error")
|
||||
|
||||
validator := &ProtobufValidatorMock{}
|
||||
validator.On("Validate", mock.Anything).Return(expectedError)
|
||||
var protobufValidator ProtobufValidator = validator
|
||||
|
||||
auditApi := LegacyAuditApi{validator: &protobufValidator}
|
||||
|
||||
event := newSystemAuditEvent(nil)
|
||||
err := auditApi.Log(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
|
||||
assert.ErrorIs(t, err, expectedError)
|
||||
}
|
||||
|
||||
func TestLegacyAuditApi_Log_NilEvent(t *testing.T) {
|
||||
auditApi := LegacyAuditApi{}
|
||||
err := auditApi.Log(context.Background(), nil, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
|
||||
assert.ErrorIs(t, err, ErrEventNil)
|
||||
}
|
||||
|
||||
func TestLegacyAuditApi_ConvertAndSerializeIntoLegacyFormatInvalidObjectIdentifierType(t *testing.T) {
|
||||
customization := func(event *auditV1.AuditLogEntry,
|
||||
objectIdentifier *auditV1.ObjectIdentifier) {
|
||||
objectIdentifier.Type = "invalid"
|
||||
}
|
||||
event, objectIdentifier := newProjectAuditEvent(&customization)
|
||||
|
||||
validator := &ProtobufValidatorMock{}
|
||||
validator.On("Validate", mock.Anything).Return(nil)
|
||||
var protobufValidator ProtobufValidator = validator
|
||||
|
||||
auditApi := LegacyAuditApi{validator: &protobufValidator}
|
||||
_, err := auditApi.ValidateAndSerialize(event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier(objectIdentifier))
|
||||
assert.ErrorIs(t, err, ErrUnsupportedRoutableType)
|
||||
}
|
||||
111
audit/api/api_mock.go
Normal file
111
audit/api/api_mock.go
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||
|
||||
"github.com/bufbuild/protovalidate-go"
|
||||
)
|
||||
|
||||
// MockAuditApi is an implementation of AuditApi that does nothing and has no dependency to external systems.
|
||||
type MockAuditApi struct {
|
||||
validator *ProtobufValidator
|
||||
}
|
||||
|
||||
func NewMockAuditApi() (*AuditApi, error) {
|
||||
validator, err := protovalidate.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var protobufValidator ProtobufValidator = validator
|
||||
var auditApi AuditApi = &MockAuditApi{validator: &protobufValidator}
|
||||
return &auditApi, nil
|
||||
}
|
||||
|
||||
// Log implements AuditApi.Log.
|
||||
// Validates and serializes the event but doesn't send it.
|
||||
func (a *MockAuditApi) Log(
|
||||
ctx context.Context,
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
) error {
|
||||
|
||||
return a.LogWithTrace(ctx, event, visibility, routableIdentifier, nil, nil)
|
||||
}
|
||||
|
||||
// LogWithTrace implements AuditApi.LogWithTrace.
|
||||
// Validates and serializes the event but doesn't send it.
|
||||
func (a *MockAuditApi) LogWithTrace(
|
||||
_ context.Context,
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
traceParent *string,
|
||||
traceState *string,
|
||||
) error {
|
||||
|
||||
_, err := a.ValidateAndSerializeWithTrace(event, visibility, routableIdentifier, traceParent, traceState)
|
||||
return err
|
||||
}
|
||||
|
||||
// ValidateAndSerialize implements AuditApi.ValidateAndSerialize
|
||||
func (a *MockAuditApi) ValidateAndSerialize(
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
) (*CloudEvent, error) {
|
||||
|
||||
return a.ValidateAndSerializeWithTrace(event, visibility, routableIdentifier, nil, nil)
|
||||
}
|
||||
|
||||
// ValidateAndSerializeWithTrace implements AuditApi.ValidateAndSerializeWithTrace
|
||||
func (a *MockAuditApi) ValidateAndSerializeWithTrace(
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
traceParent *string,
|
||||
traceState *string,
|
||||
) (*CloudEvent, error) {
|
||||
|
||||
routableEvent, err := validateAndSerializePartially(a.validator, event, visibility, routableIdentifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reject event type data-access as the downstream services
|
||||
// cannot handle it at the moment
|
||||
if strings.HasSuffix(event.LogName, string(EventTypeDataAccess)) {
|
||||
return nil, ErrUnsupportedEventTypeDataAccess
|
||||
}
|
||||
|
||||
routableEventBytes, err := proto.Marshal(routableEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
message := CloudEvent{
|
||||
SpecVersion: "1.0",
|
||||
Source: event.ProtoPayload.ServiceName,
|
||||
Id: event.InsertId,
|
||||
Time: event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(),
|
||||
DataContentType: "application/cloudevents+protobuf",
|
||||
DataType: fmt.Sprintf("%v", routableEvent.ProtoReflect().Descriptor().FullName()),
|
||||
Subject: event.ProtoPayload.ResourceName,
|
||||
Data: routableEventBytes,
|
||||
TraceParent: traceParent,
|
||||
TraceState: traceState,
|
||||
}
|
||||
|
||||
return &message, nil
|
||||
}
|
||||
|
||||
// Send implements AuditApi.Send
|
||||
func (a *MockAuditApi) Send(context.Context, *RoutableIdentifier, *CloudEvent) error {
|
||||
return nil
|
||||
}
|
||||
61
audit/api/api_mock_test.go
Normal file
61
audit/api/api_mock_test.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMockAuditApi_Log(t *testing.T) {
|
||||
|
||||
auditApi, err := NewMockAuditApi()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newOrganizationAuditEvent(nil)
|
||||
routableObjectIdentifier := NewRoutableIdentifier(objectIdentifier)
|
||||
|
||||
// Test
|
||||
t.Run("Log", func(t *testing.T) {
|
||||
assert.Nil(t, (*auditApi).Log(
|
||||
context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, routableObjectIdentifier))
|
||||
})
|
||||
|
||||
t.Run("reject data access event", func(t *testing.T) {
|
||||
event, objectIdentifier := newOrganizationAuditEvent(nil)
|
||||
event.LogName = strings.Replace(event.LogName, string(EventTypeAdminActivity), string(EventTypeDataAccess), 1)
|
||||
routableObjectIdentifier := NewRoutableIdentifier(objectIdentifier)
|
||||
|
||||
assert.ErrorIs(t, (*auditApi).Log(
|
||||
context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, routableObjectIdentifier),
|
||||
ErrUnsupportedEventTypeDataAccess)
|
||||
})
|
||||
|
||||
t.Run("ValidateAndSerialize", func(t *testing.T) {
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
cloudEvent, err := (*auditApi).ValidateAndSerializeWithTrace(
|
||||
event, visibility, routableObjectIdentifier, nil, nil)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateRoutableEventPayload(
|
||||
t, cloudEvent.Data, objectIdentifier, event, event.ProtoPayload.OperationName, visibility)
|
||||
})
|
||||
|
||||
t.Run("ValidateAndSerialize event nil", func(t *testing.T) {
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
_, err := (*auditApi).ValidateAndSerialize(nil, visibility, routableObjectIdentifier)
|
||||
|
||||
assert.ErrorIs(t, err, ErrEventNil)
|
||||
})
|
||||
|
||||
t.Run("Send", func(t *testing.T) {
|
||||
var cloudEvent = CloudEvent{}
|
||||
|
||||
assert.Nil(t, (*auditApi).Send(context.Background(), routableObjectIdentifier, &cloudEvent))
|
||||
})
|
||||
}
|
||||
198
audit/api/api_routable.go
Normal file
198
audit/api/api_routable.go
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/messaging"
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||
)
|
||||
|
||||
// routableTopicNameResolver implements TopicNameResolver.
|
||||
// Resolves topic names by concatenating topic type prefixes with routing identifiers.
|
||||
type routableTopicNameResolver struct {
|
||||
folderTopicPrefix string
|
||||
organizationTopicPrefix string
|
||||
projectTopicPrefix string
|
||||
// If no identifier is provided for routing, it will be routed to a system topic
|
||||
systemTopicName string
|
||||
}
|
||||
|
||||
// Resolve implements TopicNameResolver.Resolve.
|
||||
func (r *routableTopicNameResolver) Resolve(routableIdentifier *RoutableIdentifier) (string, error) {
|
||||
|
||||
if routableIdentifier == nil {
|
||||
return "", ErrObjectIdentifierNil
|
||||
}
|
||||
|
||||
switch routableIdentifier.Type {
|
||||
case ObjectTypeOrganization:
|
||||
return fmt.Sprintf("topic://%s/%s", r.organizationTopicPrefix, routableIdentifier.Identifier), nil
|
||||
case ObjectTypeProject:
|
||||
return fmt.Sprintf("topic://%s/%s", r.projectTopicPrefix, routableIdentifier.Identifier), nil
|
||||
case ObjectTypeFolder:
|
||||
return fmt.Sprintf("topic://%s/%s", r.folderTopicPrefix, routableIdentifier.Identifier), nil
|
||||
case ObjectTypeSystem:
|
||||
return r.systemTopicName, nil
|
||||
default:
|
||||
return "", ErrUnsupportedObjectIdentifierType
|
||||
}
|
||||
}
|
||||
|
||||
// topicNameConfig provides topic name information required for the topic name resolution.
|
||||
type topicNameConfig struct {
|
||||
FolderTopicPrefix string
|
||||
OrganizationTopicPrefix string
|
||||
ProjectTopicPrefix string
|
||||
SystemTopicName string
|
||||
}
|
||||
|
||||
// routableAuditApi is an implementation of AuditApi.
|
||||
// Warning: It is only there for local (compatibility) testing.
|
||||
// DO NOT USE IT!
|
||||
type routableAuditApi struct {
|
||||
messagingApi *messaging.Api
|
||||
topicNameResolver *TopicNameResolver
|
||||
validator *ProtobufValidator
|
||||
}
|
||||
|
||||
// NewRoutableAuditApi can be used to initialize the audit log api.
|
||||
func newRoutableAuditApi(
|
||||
messagingApi *messaging.Api,
|
||||
topicNameConfig topicNameConfig,
|
||||
validator ProtobufValidator,
|
||||
) (*AuditApi, error) {
|
||||
|
||||
if messagingApi == nil {
|
||||
return nil, errors.New("messaging api nil")
|
||||
}
|
||||
|
||||
// Topic resolver
|
||||
if topicNameConfig.FolderTopicPrefix == "" {
|
||||
return nil, errors.New("folder topic prefix is required")
|
||||
}
|
||||
if topicNameConfig.OrganizationTopicPrefix == "" {
|
||||
return nil, errors.New("organization topic prefix is required")
|
||||
}
|
||||
if topicNameConfig.ProjectTopicPrefix == "" {
|
||||
return nil, errors.New("project topic prefix is required")
|
||||
}
|
||||
if topicNameConfig.SystemTopicName == "" {
|
||||
return nil, errors.New("system topic name is required")
|
||||
}
|
||||
|
||||
var topicNameResolver TopicNameResolver = &routableTopicNameResolver{
|
||||
folderTopicPrefix: topicNameConfig.FolderTopicPrefix,
|
||||
organizationTopicPrefix: topicNameConfig.OrganizationTopicPrefix,
|
||||
projectTopicPrefix: topicNameConfig.ProjectTopicPrefix,
|
||||
systemTopicName: topicNameConfig.SystemTopicName,
|
||||
}
|
||||
|
||||
// Audit api
|
||||
var auditApi AuditApi = &routableAuditApi{
|
||||
messagingApi: messagingApi,
|
||||
topicNameResolver: &topicNameResolver,
|
||||
validator: &validator,
|
||||
}
|
||||
|
||||
return &auditApi, nil
|
||||
}
|
||||
|
||||
// Log implements AuditApi.Log
|
||||
func (a *routableAuditApi) Log(
|
||||
ctx context.Context,
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
) error {
|
||||
|
||||
return a.LogWithTrace(ctx, event, visibility, routableIdentifier, nil, nil)
|
||||
}
|
||||
|
||||
// LogWithTrace implements AuditApi.LogWithTrace
|
||||
func (a *routableAuditApi) LogWithTrace(
|
||||
ctx context.Context,
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
traceParent *string,
|
||||
traceState *string,
|
||||
) error {
|
||||
|
||||
cloudEvent, err := a.ValidateAndSerializeWithTrace(event, visibility, routableIdentifier, traceParent, traceState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.Send(ctx, routableIdentifier, cloudEvent)
|
||||
}
|
||||
|
||||
// ValidateAndSerialize implements AuditApi.ValidateAndSerialize
|
||||
func (a *routableAuditApi) ValidateAndSerialize(
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
) (*CloudEvent, error) {
|
||||
|
||||
return a.ValidateAndSerializeWithTrace(event, visibility, routableIdentifier, nil, nil)
|
||||
}
|
||||
|
||||
// ValidateAndSerializeWithTrace implements AuditApi.ValidateAndSerializeWithTrace
|
||||
func (a *routableAuditApi) ValidateAndSerializeWithTrace(
|
||||
event *auditV1.AuditLogEntry,
|
||||
visibility auditV1.Visibility,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
traceParent *string,
|
||||
traceState *string,
|
||||
) (*CloudEvent, error) {
|
||||
|
||||
routableEvent, err := validateAndSerializePartially(
|
||||
a.validator,
|
||||
event,
|
||||
visibility,
|
||||
routableIdentifier)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reject event type data-access as the downstream services
|
||||
// cannot handle it at the moment
|
||||
if strings.HasSuffix(event.LogName, string(EventTypeDataAccess)) {
|
||||
return nil, ErrUnsupportedEventTypeDataAccess
|
||||
}
|
||||
|
||||
routableEventBytes, err := proto.Marshal(routableEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
message := CloudEvent{
|
||||
SpecVersion: "1.0",
|
||||
Source: event.ProtoPayload.ServiceName,
|
||||
Id: event.InsertId,
|
||||
Time: event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(),
|
||||
DataContentType: ContentTypeCloudEventsProtobuf,
|
||||
DataType: fmt.Sprintf("%v", routableEvent.ProtoReflect().Descriptor().FullName()),
|
||||
Subject: event.ProtoPayload.ResourceName,
|
||||
Data: routableEventBytes,
|
||||
TraceParent: traceParent,
|
||||
TraceState: traceState,
|
||||
}
|
||||
|
||||
return &message, nil
|
||||
}
|
||||
|
||||
// Send implements AuditApi.Send
|
||||
func (a *routableAuditApi) Send(
|
||||
ctx context.Context,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
cloudEvent *CloudEvent,
|
||||
) error {
|
||||
|
||||
return send(a.topicNameResolver, a.messagingApi, ctx, routableIdentifier, cloudEvent)
|
||||
}
|
||||
574
audit/api/api_routable_test.go
Normal file
574
audit/api/api_routable_test.go
Normal file
|
|
@ -0,0 +1,574 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/messaging"
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||
|
||||
"github.com/Azure/go-amqp"
|
||||
"github.com/bufbuild/protovalidate-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestRoutableAuditApi(t *testing.T) {
|
||||
|
||||
// Specify test timeout
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancelFn()
|
||||
|
||||
// Start solace docker container
|
||||
solaceContainer, err := messaging.NewSolaceContainer(context.Background())
|
||||
assert.NoError(t, err)
|
||||
defer solaceContainer.Stop()
|
||||
|
||||
// Instantiate the messaging api
|
||||
messagingApi, err := messaging.NewAmqpApi(messaging.AmqpConfig{URL: solaceContainer.AmqpConnectionString})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Validator
|
||||
validator, err := protovalidate.New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
traceParent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
|
||||
traceState := "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"
|
||||
|
||||
// Instantiate the audit api
|
||||
organizationTopicPrefix := "org"
|
||||
projectTopicPrefix := "project"
|
||||
folderTopicPrefix := "folder"
|
||||
systemTopicName := "topic://system/admin-events"
|
||||
|
||||
auditApi, err := newRoutableAuditApi(
|
||||
messagingApi,
|
||||
topicNameConfig{
|
||||
FolderTopicPrefix: folderTopicPrefix,
|
||||
OrganizationTopicPrefix: organizationTopicPrefix,
|
||||
ProjectTopicPrefix: projectTopicPrefix,
|
||||
SystemTopicName: systemTopicName},
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check that event-type data-access is rejected as it is currently
|
||||
// not supported by downstream services
|
||||
t.Run("reject data access event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "org-reject-data-access"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "org/*"))
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newOrganizationAuditEvent(nil)
|
||||
event.LogName = strings.Replace(event.LogName, string(EventTypeAdminActivity), string(EventTypeDataAccess), 1)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
assert.ErrorIs(t, (*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
), ErrUnsupportedEventTypeDataAccess)
|
||||
})
|
||||
|
||||
// Check logging of organization events
|
||||
t.Run("Log public organization event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "org-event-public"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "org/*"))
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newOrganizationAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
assert.NoError(t, (*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentEvent(
|
||||
t,
|
||||
organizationTopicPrefix,
|
||||
message,
|
||||
objectIdentifier,
|
||||
event,
|
||||
"stackit.resourcemanager.v2.organization.created",
|
||||
visibility,
|
||||
&traceParent,
|
||||
&traceState)
|
||||
})
|
||||
|
||||
t.Run("Log private organization event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
queueName := "org-event-private"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "org/*"))
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newOrganizationAuditEvent(nil)
|
||||
topicName := fmt.Sprintf("org/%s", objectIdentifier.Identifier)
|
||||
assert.NoError(
|
||||
t,
|
||||
solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicName))
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
assert.NoError(t,
|
||||
(*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
// Receive the event from solace
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentEvent(
|
||||
t,
|
||||
organizationTopicPrefix,
|
||||
message,
|
||||
objectIdentifier,
|
||||
event,
|
||||
"stackit.resourcemanager.v2.organization.created",
|
||||
visibility,
|
||||
&traceParent,
|
||||
&traceState)
|
||||
})
|
||||
|
||||
// Check logging of folder events
|
||||
t.Run("Log public folder event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "folder-event-public"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "folder/*"))
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newFolderAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
assert.NoError(t, (*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentEvent(
|
||||
t,
|
||||
folderTopicPrefix,
|
||||
message,
|
||||
objectIdentifier,
|
||||
event,
|
||||
"stackit.resourcemanager.v2.folder.created",
|
||||
visibility,
|
||||
&traceParent,
|
||||
&traceState)
|
||||
})
|
||||
|
||||
t.Run("Log private folder event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
queueName := "folder-event-private"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "folder/*"))
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newFolderAuditEvent(nil)
|
||||
topicName := fmt.Sprintf("folder/%s", objectIdentifier.Identifier)
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicName))
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
assert.NoError(t,
|
||||
(*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
// Receive the event from solace
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentEvent(
|
||||
t,
|
||||
folderTopicPrefix,
|
||||
message,
|
||||
objectIdentifier,
|
||||
event,
|
||||
"stackit.resourcemanager.v2.folder.created",
|
||||
visibility,
|
||||
&traceParent,
|
||||
&traceState)
|
||||
})
|
||||
|
||||
// Check logging of project events
|
||||
t.Run("Log public project event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
queueName := "project-event-public"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "project/*"))
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newProjectAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
assert.NoError(t,
|
||||
(*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
// Receive the event from solace
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentEvent(
|
||||
t,
|
||||
projectTopicPrefix,
|
||||
message,
|
||||
objectIdentifier,
|
||||
event,
|
||||
"stackit.resourcemanager.v2.project.created",
|
||||
visibility,
|
||||
&traceParent,
|
||||
&traceState)
|
||||
})
|
||||
|
||||
t.Run("Log private project event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
queueName := "project-event-private"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "project/*"))
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newProjectAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
assert.NoError(t,
|
||||
(*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
// Receive the event from solace
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentEvent(
|
||||
t,
|
||||
projectTopicPrefix,
|
||||
message,
|
||||
objectIdentifier,
|
||||
event,
|
||||
"stackit.resourcemanager.v2.project.created",
|
||||
visibility,
|
||||
&traceParent,
|
||||
&traceState)
|
||||
})
|
||||
|
||||
// Check logging of system events with identifier
|
||||
t.Run("Log private project system event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
queueName := "project-system-event-private"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "system/*"))
|
||||
|
||||
// Instantiate test data
|
||||
event := newProjectSystemAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
assert.NoError(t,
|
||||
(*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
RoutableSystemIdentifier,
|
||||
nil,
|
||||
nil,
|
||||
))
|
||||
|
||||
// Receive the event from solace
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check topic name
|
||||
assert.Equal(t, systemTopicName, *message.Properties.To)
|
||||
|
||||
// Check cloud event properties
|
||||
applicationProperties := message.ApplicationProperties
|
||||
assert.Equal(t, "1.0", applicationProperties["cloudEvents:specversion"])
|
||||
assert.Equal(t, "resource-manager", applicationProperties["cloudEvents:source"])
|
||||
_, isUuid := uuid.Parse(fmt.Sprintf("%s", applicationProperties["cloudEvents:id"]))
|
||||
assert.True(t, true, isUuid)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime().UnixMilli(), applicationProperties["cloudEvents:time"])
|
||||
assert.Equal(t, "application/cloudevents+protobuf", applicationProperties["cloudEvents:datacontenttype"])
|
||||
assert.Equal(t, "audit.v1.RoutableAuditEvent", applicationProperties["cloudEvents:type"])
|
||||
assert.Nil(t, applicationProperties["cloudEvents:traceparent"])
|
||||
assert.Nil(t, applicationProperties["cloudEvents:tracestate"])
|
||||
|
||||
// Check deserialized message
|
||||
validateRoutableEventPayload(
|
||||
t,
|
||||
message.Data[0],
|
||||
RoutableSystemIdentifier.ToObjectIdentifier(),
|
||||
event,
|
||||
"stackit.resourcemanager.v2.system.changed",
|
||||
visibility)
|
||||
})
|
||||
|
||||
// Check logging of system events
|
||||
t.Run("Log private system event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
queueName := "system-event-private"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "system/*"))
|
||||
|
||||
// Instantiate test data
|
||||
event := newSystemAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
assert.NoError(t,
|
||||
(*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
RoutableSystemIdentifier,
|
||||
nil,
|
||||
nil,
|
||||
))
|
||||
|
||||
// Receive the event from solace
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check topic name
|
||||
assert.Equal(t, systemTopicName, *message.Properties.To)
|
||||
|
||||
// Check cloud event properties
|
||||
applicationProperties := message.ApplicationProperties
|
||||
assert.Equal(t, "1.0", applicationProperties["cloudEvents:specversion"])
|
||||
assert.Equal(t, "resource-manager", applicationProperties["cloudEvents:source"])
|
||||
_, isUuid := uuid.Parse(fmt.Sprintf("%s", applicationProperties["cloudEvents:id"]))
|
||||
assert.True(t, true, isUuid)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime().UnixMilli(), applicationProperties["cloudEvents:time"])
|
||||
assert.Equal(t, "application/cloudevents+protobuf", applicationProperties["cloudEvents:datacontenttype"])
|
||||
assert.Equal(t, "audit.v1.RoutableAuditEvent", applicationProperties["cloudEvents:type"])
|
||||
assert.Nil(t, applicationProperties["cloudEvents:traceparent"])
|
||||
assert.Nil(t, applicationProperties["cloudEvents:tracestate"])
|
||||
|
||||
// Check deserialized message
|
||||
validateRoutableEventPayload(
|
||||
t,
|
||||
message.Data[0],
|
||||
SystemIdentifier,
|
||||
event,
|
||||
"stackit.resourcemanager.v2.system.changed",
|
||||
visibility)
|
||||
})
|
||||
|
||||
// Check logging of organization events
|
||||
t.Run("Log event with details", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "org-event-with-details"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "org/*"))
|
||||
|
||||
// Instantiate test data
|
||||
event, objectIdentifier := newOrganizationAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
assert.NoError(t, (*auditApi).LogWithTrace(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
NewRoutableIdentifier(objectIdentifier),
|
||||
&traceParent,
|
||||
&traceState,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentEvent(
|
||||
t,
|
||||
organizationTopicPrefix,
|
||||
message,
|
||||
objectIdentifier,
|
||||
event,
|
||||
"stackit.resourcemanager.v2.organization.created",
|
||||
visibility,
|
||||
&traceParent,
|
||||
&traceState)
|
||||
})
|
||||
}
|
||||
|
||||
func validateSentEvent(
|
||||
t *testing.T,
|
||||
topicPrefix string,
|
||||
message *amqp.Message,
|
||||
objectIdentifier *auditV1.ObjectIdentifier,
|
||||
event *auditV1.AuditLogEntry,
|
||||
operationName string,
|
||||
visibility auditV1.Visibility,
|
||||
traceParent *string,
|
||||
traceState *string,
|
||||
) {
|
||||
|
||||
// Check topic name
|
||||
assert.Equal(t,
|
||||
fmt.Sprintf("topic://%s/%s", topicPrefix, objectIdentifier.Identifier),
|
||||
*message.Properties.To)
|
||||
|
||||
// Check cloud event properties
|
||||
applicationProperties := message.ApplicationProperties
|
||||
assert.Equal(t, "1.0", applicationProperties["cloudEvents:specversion"])
|
||||
assert.Equal(t, "resource-manager", applicationProperties["cloudEvents:source"])
|
||||
_, isUuid := uuid.Parse(fmt.Sprintf("%s", applicationProperties["cloudEvents:id"]))
|
||||
assert.True(t, true, isUuid)
|
||||
assert.Equal(t, event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime().UnixMilli(), applicationProperties["cloudEvents:time"])
|
||||
assert.Equal(t, ContentTypeCloudEventsProtobuf, applicationProperties["cloudEvents:datacontenttype"])
|
||||
assert.Equal(t, "audit.v1.RoutableAuditEvent", applicationProperties["cloudEvents:type"])
|
||||
assert.Equal(t, *traceParent, applicationProperties["cloudEvents:traceparent"])
|
||||
assert.Equal(t, *traceState, applicationProperties["cloudEvents:tracestate"])
|
||||
|
||||
// Check deserialized message
|
||||
validateRoutableEventPayload(
|
||||
t, message.Data[0], objectIdentifier, event, operationName, visibility)
|
||||
}
|
||||
|
||||
func validateRoutableEventPayload(
|
||||
t *testing.T,
|
||||
payload []byte,
|
||||
objectIdentifier *auditV1.ObjectIdentifier,
|
||||
event *auditV1.AuditLogEntry,
|
||||
operationName string,
|
||||
visibility auditV1.Visibility,
|
||||
) {
|
||||
|
||||
// Check routable audit event parameters
|
||||
var routableAuditEvent auditV1.RoutableAuditEvent
|
||||
assert.NoError(t, proto.Unmarshal(payload, &routableAuditEvent))
|
||||
|
||||
assert.Equal(t, operationName, routableAuditEvent.OperationName)
|
||||
assert.Equal(t, visibility, routableAuditEvent.Visibility)
|
||||
|
||||
assert.True(t, proto.Equal(objectIdentifier, routableAuditEvent.ObjectIdentifier))
|
||||
|
||||
var auditEvent auditV1.AuditLogEntry
|
||||
switch data := routableAuditEvent.Data.(type) {
|
||||
case *auditV1.RoutableAuditEvent_UnencryptedData:
|
||||
assert.NoError(t, proto.Unmarshal(data.UnencryptedData.Data, &auditEvent))
|
||||
default:
|
||||
assert.Fail(t, "Encrypted data not expected")
|
||||
}
|
||||
|
||||
// Check audit event
|
||||
assert.True(t, proto.Equal(event, &auditEvent))
|
||||
}
|
||||
|
||||
func TestRoutableTopicNameResolver_Resolve_UnsupportedIdentifierType(t *testing.T) {
|
||||
resolver := routableTopicNameResolver{}
|
||||
_, err := resolver.Resolve(NewRoutableIdentifier(&auditV1.ObjectIdentifier{Type: "unsupported"}))
|
||||
assert.ErrorIs(t, err, ErrUnsupportedObjectIdentifierType)
|
||||
}
|
||||
|
||||
func TestNewRoutableAuditApi_NewRoutableAuditApi_MessagingApiNil(t *testing.T) {
|
||||
auditApi, err := newRoutableAuditApi(nil, topicNameConfig{}, nil)
|
||||
assert.Nil(t, auditApi)
|
||||
assert.EqualError(t, err, "messaging api nil")
|
||||
}
|
||||
|
||||
func TestRoutableAuditApi_ValidateAndSerialize_ValidationFailed(t *testing.T) {
|
||||
expectedError := errors.New("expected error")
|
||||
|
||||
validator := &ProtobufValidatorMock{}
|
||||
validator.On("Validate", mock.Anything).Return(expectedError)
|
||||
var protobufValidator ProtobufValidator = validator
|
||||
|
||||
auditApi := routableAuditApi{validator: &protobufValidator}
|
||||
|
||||
event := newSystemAuditEvent(nil)
|
||||
_, err := auditApi.ValidateAndSerialize(event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
|
||||
assert.ErrorIs(t, err, expectedError)
|
||||
}
|
||||
|
||||
func TestRoutableAuditApi_Log_ValidationFailed(t *testing.T) {
|
||||
expectedError := errors.New("expected error")
|
||||
|
||||
validator := &ProtobufValidatorMock{}
|
||||
validator.On("Validate", mock.Anything).Return(expectedError)
|
||||
var protobufValidator ProtobufValidator = validator
|
||||
|
||||
auditApi := routableAuditApi{validator: &protobufValidator}
|
||||
|
||||
event := newSystemAuditEvent(nil)
|
||||
err := auditApi.LogWithTrace(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier, nil, nil)
|
||||
assert.ErrorIs(t, err, expectedError)
|
||||
}
|
||||
|
||||
func TestRoutableAuditApi_Log_NilEvent(t *testing.T) {
|
||||
auditApi := routableAuditApi{}
|
||||
err := auditApi.Log(context.Background(), nil, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier)
|
||||
assert.ErrorIs(t, err, ErrEventNil)
|
||||
}
|
||||
70
audit/api/base64.go
Normal file
70
audit/api/base64.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const base64AuditEventV1 = "v1"
|
||||
|
||||
var ErrBase64StringEmpty = errors.New("base64 string must not be empty")
|
||||
var ErrRoutableIdentifierNil = errors.New("routableIdentifier must not be nil")
|
||||
var ErrUnsupportedBase64StringVersion = errors.New("unsupported base64 cloud event string version")
|
||||
|
||||
type serializableEvent struct {
|
||||
CloudEvent CloudEvent `json:"cloudEvent"`
|
||||
RoutableIdentifier RoutableIdentifier `json:"routableIdentifier"`
|
||||
}
|
||||
|
||||
func ToBase64(
|
||||
cloudEvent *CloudEvent,
|
||||
routableIdentifier *RoutableIdentifier) (*string, error) {
|
||||
|
||||
if cloudEvent == nil {
|
||||
return nil, ErrCloudEventNil
|
||||
}
|
||||
|
||||
if routableIdentifier == nil {
|
||||
return nil, ErrRoutableIdentifierNil
|
||||
}
|
||||
|
||||
event := serializableEvent{
|
||||
CloudEvent: *cloudEvent,
|
||||
RoutableIdentifier: *routableIdentifier,
|
||||
}
|
||||
|
||||
serializedEvent, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
base64Str := base64.StdEncoding.EncodeToString(serializedEvent)
|
||||
base64Str = base64Str + base64AuditEventV1
|
||||
return &base64Str, nil
|
||||
}
|
||||
|
||||
func FromBase64(base64Str string) (*CloudEvent, *RoutableIdentifier, error) {
|
||||
if base64Str == "" {
|
||||
return nil, nil, ErrBase64StringEmpty
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(base64Str, base64AuditEventV1) {
|
||||
return nil, nil, ErrUnsupportedBase64StringVersion
|
||||
}
|
||||
base64Str = strings.TrimSuffix(base64Str, base64AuditEventV1)
|
||||
|
||||
base64Bytes, err := base64.StdEncoding.DecodeString(base64Str)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
event := serializableEvent{}
|
||||
err = json.Unmarshal(base64Bytes, &event)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &event.CloudEvent, &event.RoutableIdentifier, nil
|
||||
}
|
||||
85
audit/api/base64_test.go
Normal file
85
audit/api/base64_test.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_ToBase64(t *testing.T) {
|
||||
|
||||
t.Run("cloud event nil", func(t *testing.T) {
|
||||
var cloudEvent *CloudEvent = nil
|
||||
routableIdentifier := RoutableSystemIdentifier
|
||||
|
||||
base64str, err := ToBase64(cloudEvent, routableIdentifier)
|
||||
assert.ErrorIs(t, err, ErrCloudEventNil)
|
||||
assert.Nil(t, base64str)
|
||||
})
|
||||
|
||||
t.Run("routable identifier nil", func(t *testing.T) {
|
||||
cloudEvent := &CloudEvent{}
|
||||
var routableIdentifier *RoutableIdentifier = nil
|
||||
|
||||
base64str, err := ToBase64(cloudEvent, routableIdentifier)
|
||||
assert.ErrorIs(t, err, ErrRoutableIdentifierNil)
|
||||
assert.Nil(t, base64str)
|
||||
})
|
||||
|
||||
t.Run("encoded event", func(t *testing.T) {
|
||||
e := &CloudEvent{}
|
||||
r := RoutableSystemIdentifier
|
||||
base64str, err := ToBase64(e, r)
|
||||
assert.NoError(t, err)
|
||||
|
||||
cloudEvent, routableIdentifier, err := FromBase64(*base64str)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, e, cloudEvent)
|
||||
assert.Equal(t, r, routableIdentifier)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_FromBase64(t *testing.T) {
|
||||
|
||||
t.Run("empty string", func(t *testing.T) {
|
||||
cloudEvent, routableIdentifier, err := FromBase64("")
|
||||
assert.ErrorIs(t, err, ErrBase64StringEmpty)
|
||||
assert.Nil(t, cloudEvent)
|
||||
assert.Nil(t, routableIdentifier)
|
||||
})
|
||||
|
||||
t.Run("without version suffix", func(t *testing.T) {
|
||||
cloudEvent, routableIdentifier, err := FromBase64("ey")
|
||||
assert.ErrorIs(t, err, ErrUnsupportedBase64StringVersion)
|
||||
assert.Nil(t, cloudEvent)
|
||||
assert.Nil(t, routableIdentifier)
|
||||
})
|
||||
|
||||
t.Run("no base64 string", func(t *testing.T) {
|
||||
cloudEvent, routableIdentifier, err := FromBase64("no base 64 v1")
|
||||
assert.EqualError(t, err, "illegal base64 data at input byte 2")
|
||||
assert.Nil(t, cloudEvent)
|
||||
assert.Nil(t, routableIdentifier)
|
||||
})
|
||||
|
||||
t.Run("no json serialized event", func(t *testing.T) {
|
||||
base64Str := base64.StdEncoding.EncodeToString([]byte("not expected"))
|
||||
base64Str = base64Str + base64AuditEventV1
|
||||
cloudEvent, routableIdentifier, err := FromBase64(base64Str)
|
||||
assert.EqualError(t, err, "invalid character 'o' in literal null (expecting 'u')")
|
||||
assert.Nil(t, cloudEvent)
|
||||
assert.Nil(t, routableIdentifier)
|
||||
})
|
||||
|
||||
t.Run("decoded event", func(t *testing.T) {
|
||||
e := &CloudEvent{}
|
||||
r := RoutableSystemIdentifier
|
||||
base64str, err := ToBase64(e, r)
|
||||
assert.NoError(t, err)
|
||||
|
||||
cloudEvent, routableIdentifier, err := FromBase64(*base64str)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, e, cloudEvent)
|
||||
assert.Equal(t, r, routableIdentifier)
|
||||
})
|
||||
}
|
||||
662
audit/api/builder.go
Normal file
662
audit/api/builder.go
Normal file
|
|
@ -0,0 +1,662 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/utils"
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"time"
|
||||
)
|
||||
|
||||
const quadZero = "0.0.0.0"
|
||||
|
||||
type SequenceNumber uint64
|
||||
|
||||
type AuditParameters struct {
|
||||
|
||||
// A map that is added as "details" to the message
|
||||
Details map[string]interface{}
|
||||
|
||||
// The type of the event
|
||||
EventType EventType
|
||||
|
||||
// A set of user-defined (key, value) data that provides additional
|
||||
// information about the log entry.
|
||||
Labels map[string]string
|
||||
|
||||
// UUID identifier of the object, the audit event refers to
|
||||
ObjectId string
|
||||
|
||||
// Type of the object, the audit event refers to
|
||||
ObjectType ObjectType
|
||||
|
||||
ResponseBody any
|
||||
|
||||
// Log severity
|
||||
Severity auditV1.LogSeverity
|
||||
}
|
||||
|
||||
func getObjectIdAndTypeFromAuditParams(
|
||||
auditParams *AuditParameters,
|
||||
) (string, *ObjectType, error) {
|
||||
|
||||
objectId := auditParams.ObjectId
|
||||
if objectId == "" {
|
||||
return "", nil, errors.New("object id missing")
|
||||
}
|
||||
|
||||
var objectType *ObjectType
|
||||
if auditParams.ObjectType != "" {
|
||||
objectType = &auditParams.ObjectType
|
||||
}
|
||||
|
||||
if objectType == nil {
|
||||
return "", nil, errors.New("object type missing")
|
||||
}
|
||||
if err := objectType.IsSupportedType(); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return objectId, objectType, nil
|
||||
}
|
||||
|
||||
// AuditLogEntryBuilder collects audit params to construct auditV1.AuditLogEntry
|
||||
type AuditLogEntryBuilder struct {
|
||||
auditParams AuditParameters
|
||||
auditRequest AuditRequest
|
||||
auditResponse AuditResponse
|
||||
auditMetadata AuditMetadata
|
||||
|
||||
// Region and optional zone id. If both, separated with a - (dash).
|
||||
// Example: eu01
|
||||
location string
|
||||
|
||||
// The ID of the K8s Pod, Service-Instance, etc. (must be unique for a sending service)
|
||||
workerId string
|
||||
}
|
||||
|
||||
// NewAuditLogEntryBuilder returns a builder to construct auditV1.AuditLogEntry
|
||||
func NewAuditLogEntryBuilder() *AuditLogEntryBuilder {
|
||||
|
||||
requestTime := time.Now().UTC()
|
||||
|
||||
return &AuditLogEntryBuilder{
|
||||
auditParams: AuditParameters{
|
||||
EventType: EventTypeAdminActivity,
|
||||
},
|
||||
auditRequest: AuditRequest{
|
||||
Request: &ApiRequest{},
|
||||
RequestClientIP: quadZero,
|
||||
RequestCorrelationId: nil,
|
||||
RequestId: nil,
|
||||
RequestTime: &requestTime,
|
||||
},
|
||||
auditResponse: AuditResponse{
|
||||
ResponseBodyBytes: nil,
|
||||
ResponseStatusCode: 200,
|
||||
ResponseHeaders: make(map[string][]string),
|
||||
ResponseNumItems: nil,
|
||||
ResponseTime: nil,
|
||||
},
|
||||
auditMetadata: AuditMetadata{
|
||||
AuditInsertId: "",
|
||||
AuditLabels: nil,
|
||||
AuditLogName: "",
|
||||
AuditLogSeverity: auditV1.LogSeverity_LOG_SEVERITY_DEFAULT,
|
||||
AuditOperationName: "",
|
||||
AuditPermission: nil,
|
||||
AuditPermissionGranted: nil,
|
||||
AuditResourceName: "",
|
||||
AuditServiceName: "",
|
||||
AuditTime: nil,
|
||||
},
|
||||
location: "",
|
||||
workerId: "",
|
||||
}
|
||||
}
|
||||
|
||||
func (builder *AuditLogEntryBuilder) AsSystemEvent() *AuditLogEntryBuilder {
|
||||
if builder.auditRequest.Request == nil {
|
||||
builder.auditRequest.Request = &ApiRequest{}
|
||||
}
|
||||
if builder.auditRequest.Request.Header == nil {
|
||||
builder.auditRequest.Request.Header = map[string][]string{"user-agent": {"none"}}
|
||||
}
|
||||
if builder.auditRequest.Request.Host == "" {
|
||||
builder.auditRequest.Request.Host = quadZero
|
||||
}
|
||||
if builder.auditRequest.Request.Method == "" {
|
||||
builder.auditRequest.Request.Method = "OTHER"
|
||||
}
|
||||
if builder.auditRequest.Request.Scheme == "" {
|
||||
builder.auditRequest.Request.Scheme = "none"
|
||||
}
|
||||
if builder.auditRequest.Request.Proto == "" {
|
||||
builder.auditRequest.Request.Proto = "none"
|
||||
}
|
||||
if builder.auditRequest.Request.URL.Path == "" {
|
||||
builder.auditRequest.Request.URL.Path = "none"
|
||||
}
|
||||
if builder.auditRequest.RequestClientIP == "" {
|
||||
builder.auditRequest.RequestClientIP = quadZero
|
||||
}
|
||||
builder.WithEventType(EventTypeSystemEvent)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithRequiredApiRequest adds api request details
|
||||
func (builder *AuditLogEntryBuilder) WithRequiredApiRequest(request ApiRequest) *AuditLogEntryBuilder {
|
||||
builder.auditRequest.Request = &request
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithRequiredLocation adds the region and optional zone id. If both, separated with a - (dash).
|
||||
// Example: eu01
|
||||
func (builder *AuditLogEntryBuilder) WithRequiredLocation(location string) *AuditLogEntryBuilder {
|
||||
builder.location = location
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithRequiredRequestClientIp adds the client ip
|
||||
func (builder *AuditLogEntryBuilder) WithRequiredRequestClientIp(requestClientIp string) *AuditLogEntryBuilder {
|
||||
builder.auditRequest.RequestClientIP = requestClientIp
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithRequestCorrelationId adds an optional request correlation id
|
||||
func (builder *AuditLogEntryBuilder) WithRequestCorrelationId(requestCorrelationId string) *AuditLogEntryBuilder {
|
||||
builder.auditRequest.RequestCorrelationId = &requestCorrelationId
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithRequestId adds an optional request id
|
||||
func (builder *AuditLogEntryBuilder) WithRequestId(requestId string) *AuditLogEntryBuilder {
|
||||
builder.auditRequest.RequestId = &requestId
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithRequestTime sets the request time on the builder. If not set - the instantiation time of the builder is used.
|
||||
func (builder *AuditLogEntryBuilder) WithRequestTime(requestTime time.Time) *AuditLogEntryBuilder {
|
||||
builder.auditRequest.RequestTime = &requestTime
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithRequiredServiceName adds the service name in lowercase (allowed characters are [a-z-]).
|
||||
func (builder *AuditLogEntryBuilder) WithRequiredServiceName(serviceName string) *AuditLogEntryBuilder {
|
||||
builder.auditMetadata.AuditServiceName = serviceName
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithRequiredWorkerId adds the ID of the K8s Pod, Service-Instance, etc. (must be unique for a sending service)
|
||||
func (builder *AuditLogEntryBuilder) WithRequiredWorkerId(workerId string) *AuditLogEntryBuilder {
|
||||
builder.workerId = workerId
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithRequiredObjectId adds the object identifier.
|
||||
// May be prefilled by audit middleware (if the identifier can be extracted from the url path).
|
||||
func (builder *AuditLogEntryBuilder) WithRequiredObjectId(objectId string) *AuditLogEntryBuilder {
|
||||
builder.auditParams.ObjectId = objectId
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithRequiredObjectType adds the object type.
|
||||
// May be prefilled by audit middleware (if the type can be extracted from the url path).
|
||||
func (builder *AuditLogEntryBuilder) WithRequiredObjectType(objectType ObjectType) *AuditLogEntryBuilder {
|
||||
builder.auditParams.ObjectType = objectType
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithRequiredOperation adds the name of the service method or operation.
|
||||
//
|
||||
// Format: stackit.<product>.<version>.<type-chain>.<operation>
|
||||
// Where:
|
||||
//
|
||||
// Product: The name of the service in lowercase
|
||||
// Version: Optional API version
|
||||
// Type-Chain: Chained path to object
|
||||
// Operation: The name of the operation in lowercase
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// "stackit.resource-manager.v1.organizations.create"
|
||||
// "stackit.authorization.v1.projects.volumes.create"
|
||||
// "stackit.authorization.v2alpha.projects.volumes.create"
|
||||
// "stackit.authorization.v2.folders.move"
|
||||
// "stackit.resource-manager.health"
|
||||
func (builder *AuditLogEntryBuilder) WithRequiredOperation(operation string) *AuditLogEntryBuilder {
|
||||
builder.auditMetadata.AuditOperationName = operation
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithAuditPermission adds the IAM permission
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// "resourcemanager.project.edit"
|
||||
func (builder *AuditLogEntryBuilder) WithAuditPermission(permission string) *AuditLogEntryBuilder {
|
||||
builder.auditMetadata.AuditPermission = &permission
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithAuditPermissionCheckResult adds the IAM permission check result
|
||||
func (builder *AuditLogEntryBuilder) WithAuditPermissionCheckResult(permissionCheckResult bool) *AuditLogEntryBuilder {
|
||||
builder.auditMetadata.AuditPermissionGranted = &permissionCheckResult
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithLabels adds A set of user-defined (key, value) data that provides additional
|
||||
// information about the log entry.
|
||||
func (builder *AuditLogEntryBuilder) WithLabels(labels map[string]string) *AuditLogEntryBuilder {
|
||||
builder.auditMetadata.AuditLabels = &labels
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithNumResponseItems adds the number of items returned to the client if applicable.
|
||||
func (builder *AuditLogEntryBuilder) WithNumResponseItems(numResponseItems int64) *AuditLogEntryBuilder {
|
||||
builder.auditResponse.ResponseNumItems = &numResponseItems
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithEventType overwrites the default event type EventTypeAdminActivity
|
||||
func (builder *AuditLogEntryBuilder) WithEventType(eventType EventType) *AuditLogEntryBuilder {
|
||||
builder.auditParams.EventType = eventType
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithDetails adds an optional details object to the audit log entry
|
||||
func (builder *AuditLogEntryBuilder) WithDetails(details map[string]interface{}) *AuditLogEntryBuilder {
|
||||
builder.auditParams.Details = details
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithSeverity overwrites the default log severity level auditV1.LogSeverity_LOG_SEVERITY_DEFAULT
|
||||
func (builder *AuditLogEntryBuilder) WithSeverity(severity auditV1.LogSeverity) *AuditLogEntryBuilder {
|
||||
builder.auditMetadata.AuditLogSeverity = severity
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithStatusCode adds the (http) response status code
|
||||
func (builder *AuditLogEntryBuilder) WithStatusCode(statusCode int) *AuditLogEntryBuilder {
|
||||
builder.auditResponse.ResponseStatusCode = statusCode
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithResponseBody adds the response body to the builder and transforms it in the Build method (json serializable or protobuf message expected)
|
||||
func (builder *AuditLogEntryBuilder) WithResponseBody(responseBody any) *AuditLogEntryBuilder {
|
||||
builder.auditParams.ResponseBody = responseBody
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithResponseBodyBytes adds the response body as bytes (serialized json or protobuf message expected)
|
||||
func (builder *AuditLogEntryBuilder) WithResponseBodyBytes(responseBody *[]byte) *AuditLogEntryBuilder {
|
||||
builder.auditResponse.ResponseBodyBytes = responseBody
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithResponseHeaders adds response headers
|
||||
func (builder *AuditLogEntryBuilder) WithResponseHeaders(responseHeaders map[string][]string) *AuditLogEntryBuilder {
|
||||
builder.auditResponse.ResponseHeaders = responseHeaders
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithResponseTime adds the time when the response is sent
|
||||
func (builder *AuditLogEntryBuilder) WithResponseTime(responseTime time.Time) *AuditLogEntryBuilder {
|
||||
builder.auditResponse.ResponseTime = &responseTime
|
||||
return builder
|
||||
}
|
||||
|
||||
// Build constructs the auditV1.AuditLogEntry.
|
||||
//
|
||||
// Parameters:
|
||||
// - A context object
|
||||
// - A SequenceNumber
|
||||
//
|
||||
// Returns:
|
||||
// - The auditV1.AuditLogEntry protobuf message or
|
||||
// - Error if the entry cannot be built
|
||||
func (builder *AuditLogEntryBuilder) Build(_ context.Context, sequenceNumber SequenceNumber) (*auditV1.AuditLogEntry, error) {
|
||||
auditTime := time.Now()
|
||||
builder.auditMetadata.AuditTime = &auditTime
|
||||
|
||||
objectId, objectType, err := getObjectIdAndTypeFromAuditParams(&builder.auditParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if builder.auditResponse.ResponseBodyBytes != nil && builder.auditParams.ResponseBody != nil {
|
||||
return nil, errors.New("responseBodyBytes and responseBody set")
|
||||
} else if builder.auditParams.ResponseBody != nil {
|
||||
responseBytes, err := ResponseBodyToBytes(builder.auditParams.ResponseBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
builder.auditResponse.ResponseBodyBytes = responseBytes
|
||||
}
|
||||
|
||||
resourceName := fmt.Sprintf("%s/%s", objectType.Plural(), objectId)
|
||||
var logIdentifier string
|
||||
var logType ObjectType
|
||||
if builder.auditParams.EventType == EventTypeSystemEvent {
|
||||
logIdentifier = SystemIdentifier.Identifier
|
||||
logType = ObjectTypeSystem
|
||||
} else {
|
||||
logIdentifier = objectId
|
||||
logType = *objectType
|
||||
}
|
||||
|
||||
builder.auditMetadata.AuditInsertId = NewInsertId(time.Now().UTC(), builder.location, builder.workerId, uint64(sequenceNumber))
|
||||
builder.auditMetadata.AuditLogName = fmt.Sprintf("%s/%s/logs/%s", logType.Plural(), logIdentifier, builder.auditParams.EventType)
|
||||
builder.auditMetadata.AuditResourceName = resourceName
|
||||
|
||||
var details *map[string]interface{} = nil
|
||||
if len(builder.auditParams.Details) > 0 {
|
||||
details = &builder.auditParams.Details
|
||||
}
|
||||
|
||||
// Instantiate the audit event
|
||||
return NewAuditLogEntry(
|
||||
builder.auditRequest,
|
||||
builder.auditResponse,
|
||||
details,
|
||||
builder.auditMetadata,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
// AuditEventBuilder collects audit log parameters, validates input and
|
||||
// returns a cloud event that can be sent to the audit log system.
|
||||
type AuditEventBuilder struct {
|
||||
|
||||
// The audit api used to validate, serialize and send events
|
||||
api *AuditApi
|
||||
|
||||
// The audit log entry builder which is used to build the actual protobuf message
|
||||
auditLogEntryBuilder *AuditLogEntryBuilder
|
||||
|
||||
// Status whether the event has been built
|
||||
built bool
|
||||
|
||||
// Sequence number generator providing sequential increasing numbers for the insert IDs
|
||||
sequenceNumberGenerator *utils.SequenceNumberGenerator
|
||||
|
||||
// Opentelemetry tracer
|
||||
tracer trace.Tracer
|
||||
|
||||
// Visibility of the event
|
||||
visibility auditV1.Visibility
|
||||
}
|
||||
|
||||
// NewAuditEventBuilder returns a builder that collects audit log parameters,
|
||||
// validates input and returns a cloud event that can be sent to the audit log system.
|
||||
func NewAuditEventBuilder(
|
||||
// The audit api used to validate, serialize and send events
|
||||
api *AuditApi,
|
||||
|
||||
// The sequence number generator can be used to get and revert sequence numbers to build audit log events
|
||||
sequenceNumberGenerator *utils.SequenceNumberGenerator,
|
||||
|
||||
// Tracer
|
||||
tracer trace.Tracer,
|
||||
|
||||
// The service name in lowercase (allowed characters are [a-z-]).
|
||||
serviceName string,
|
||||
|
||||
// The ID of the K8s Pod, Service-Instance, etc. (must be unique for a sending service)
|
||||
workerId string,
|
||||
|
||||
// The location of the service (e.g. eu01)
|
||||
location string,
|
||||
) *AuditEventBuilder {
|
||||
return &AuditEventBuilder{
|
||||
api: api,
|
||||
auditLogEntryBuilder: NewAuditLogEntryBuilder().
|
||||
WithRequiredServiceName(serviceName).
|
||||
WithRequiredWorkerId(workerId).
|
||||
WithRequiredLocation(location),
|
||||
sequenceNumberGenerator: sequenceNumberGenerator,
|
||||
tracer: tracer,
|
||||
visibility: auditV1.Visibility_VISIBILITY_PUBLIC,
|
||||
}
|
||||
}
|
||||
|
||||
// NextSequenceNumber returns the next sequence number from utils.SequenceNumberGenerator.
|
||||
// In case of an error RevertSequenceNumber must be called to prevent gaps in the sequence of numbers.
|
||||
func (builder *AuditEventBuilder) NextSequenceNumber() SequenceNumber {
|
||||
return SequenceNumber((*builder.sequenceNumberGenerator).Next())
|
||||
}
|
||||
|
||||
// RevertSequenceNumber can be called to decrease the sequence number on the utils.SequenceNumberGenerator in case of an error
|
||||
func (builder *AuditEventBuilder) RevertSequenceNumber() {
|
||||
(*builder.sequenceNumberGenerator).Revert()
|
||||
}
|
||||
|
||||
func (builder *AuditEventBuilder) AsSystemEvent() *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.AsSystemEvent()
|
||||
builder.WithVisibility(auditV1.Visibility_VISIBILITY_PRIVATE)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithAuditLogEntryBuilder overwrites the preconfigured AuditLogEntryBuilder
|
||||
func (builder *AuditEventBuilder) WithAuditLogEntryBuilder(auditLogEntryBuilder *AuditLogEntryBuilder) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder = auditLogEntryBuilder
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithRequiredApiRequest adds api request details
|
||||
func (builder *AuditEventBuilder) WithRequiredApiRequest(request ApiRequest) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.WithRequiredApiRequest(request)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithRequiredRequestClientIp adds the client ip
|
||||
func (builder *AuditEventBuilder) WithRequiredRequestClientIp(requestClientIp string) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.WithRequiredRequestClientIp(requestClientIp)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithRequestCorrelationId adds an optional request correlation id
|
||||
func (builder *AuditEventBuilder) WithRequestCorrelationId(requestCorrelationId string) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.WithRequestCorrelationId(requestCorrelationId)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithRequestId adds an optional request id
|
||||
func (builder *AuditEventBuilder) WithRequestId(requestId string) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.WithRequestId(requestId)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithRequestTime sets the request time on the builder. If not set - the instantiation time of the builder is used.
|
||||
func (builder *AuditEventBuilder) WithRequestTime(requestTime time.Time) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.WithRequestTime(requestTime)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithRequiredObjectId adds the object identifier.
|
||||
// May be prefilled by audit middleware (if the identifier can be extracted from the url path).
|
||||
func (builder *AuditEventBuilder) WithRequiredObjectId(objectId string) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.WithRequiredObjectId(objectId)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithRequiredObjectType adds the object type.
|
||||
// May be prefilled by audit middleware (if the type can be extracted from the url path).
|
||||
func (builder *AuditEventBuilder) WithRequiredObjectType(objectType ObjectType) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.WithRequiredObjectType(objectType)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithRequiredOperation adds the name of the service method or operation.
|
||||
//
|
||||
// Format: stackit.<product>.<version>.<type-chain>.<operation>
|
||||
// Where:
|
||||
//
|
||||
// Product: The name of the service in lowercase
|
||||
// Version: Optional API version
|
||||
// Type-Chain: Chained path to object
|
||||
// Operation: The name of the operation in lowercase
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// "stackit.resource-manager.v1.organizations.create"
|
||||
// "stackit.authorization.v1.projects.volumes.create"
|
||||
// "stackit.authorization.v2alpha.projects.volumes.create"
|
||||
// "stackit.authorization.v2.folders.move"
|
||||
// "stackit.resource-manager.health"
|
||||
func (builder *AuditEventBuilder) WithRequiredOperation(operation string) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.auditMetadata.AuditOperationName = operation
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithAuditPermission adds the IAM permission
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// "resourcemanager.project.edit"
|
||||
func (builder *AuditEventBuilder) WithAuditPermission(permission string) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.WithAuditPermission(permission)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithAuditPermissionCheckResult adds the IAM permission check result
|
||||
func (builder *AuditEventBuilder) WithAuditPermissionCheckResult(permissionCheckResult bool) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.WithAuditPermissionCheckResult(permissionCheckResult)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithLabels adds A set of user-defined (key, value) data that provides additional
|
||||
// information about the log entry.
|
||||
func (builder *AuditEventBuilder) WithLabels(labels map[string]string) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.WithLabels(labels)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithNumResponseItems adds the number of items returned to the client if applicable.
|
||||
func (builder *AuditEventBuilder) WithNumResponseItems(numResponseItems int64) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.WithNumResponseItems(numResponseItems)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithEventType overwrites the default event type EventTypeAdminActivity
|
||||
func (builder *AuditEventBuilder) WithEventType(eventType EventType) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.WithEventType(eventType)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithDetails adds an optional details object to the audit log entry
|
||||
func (builder *AuditEventBuilder) WithDetails(details map[string]interface{}) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.WithDetails(details)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithSeverity overwrites the default log severity level auditV1.LogSeverity_LOG_SEVERITY_DEFAULT
|
||||
func (builder *AuditEventBuilder) WithSeverity(severity auditV1.LogSeverity) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.WithSeverity(severity)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithStatusCode adds the (http) response status code
|
||||
func (builder *AuditEventBuilder) WithStatusCode(statusCode int) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.WithStatusCode(statusCode)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithResponseBody adds the response body to the builder and transforms it in the Build method (json serializable or protobuf message expected)
|
||||
func (builder *AuditEventBuilder) WithResponseBody(responseBody any) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.WithResponseBody(responseBody)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithResponseBodyBytes adds the response body as bytes (serialized json or protobuf message expected)
|
||||
func (builder *AuditEventBuilder) WithResponseBodyBytes(responseBody *[]byte) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.WithResponseBodyBytes(responseBody)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithResponseHeaders adds response headers
|
||||
func (builder *AuditEventBuilder) WithResponseHeaders(responseHeaders map[string][]string) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.WithResponseHeaders(responseHeaders)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithResponseTime adds the time when the response is sent
|
||||
func (builder *AuditEventBuilder) WithResponseTime(responseTime time.Time) *AuditEventBuilder {
|
||||
builder.auditLogEntryBuilder.WithResponseTime(responseTime)
|
||||
return builder
|
||||
}
|
||||
|
||||
// WithVisibility overwrites the default visibility auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
func (builder *AuditEventBuilder) WithVisibility(visibility auditV1.Visibility) *AuditEventBuilder {
|
||||
builder.visibility = visibility
|
||||
return builder
|
||||
}
|
||||
|
||||
// IsBuilt returns the status whether the cloud event has been built
|
||||
func (builder *AuditEventBuilder) IsBuilt() bool {
|
||||
return builder.built
|
||||
}
|
||||
|
||||
// Build constructs the CloudEvent.
|
||||
//
|
||||
// Parameters:
|
||||
// - A context object
|
||||
// - A sequence number. AuditEventBuilder.NextSequenceNumber can be used to get the next SequenceNumber.
|
||||
//
|
||||
// Returns:
|
||||
// - The CloudEvent containing the audit log entry
|
||||
// - The RoutableIdentifier required for routing the cloud event
|
||||
// - The operation name
|
||||
// - Error if the event cannot be built
|
||||
func (builder *AuditEventBuilder) Build(ctx context.Context, sequenceNumber SequenceNumber) (*CloudEvent, *RoutableIdentifier, error) {
|
||||
if builder.auditLogEntryBuilder == nil {
|
||||
return nil, nil, fmt.Errorf("audit log entry builder not set")
|
||||
}
|
||||
|
||||
objectId := builder.auditLogEntryBuilder.auditParams.ObjectId
|
||||
objectType := builder.auditLogEntryBuilder.auditParams.ObjectType
|
||||
var routingIdentifier *RoutableIdentifier
|
||||
if builder.auditLogEntryBuilder.auditParams.EventType == EventTypeSystemEvent {
|
||||
routingIdentifier = NewAuditRoutingIdentifier(uuid.Nil.String(), ObjectTypeSystem)
|
||||
if objectId == "" {
|
||||
objectId = uuid.Nil.String()
|
||||
builder.WithRequiredObjectId(objectId)
|
||||
}
|
||||
if objectType == "" {
|
||||
objectType = ObjectTypeSystem
|
||||
builder.WithRequiredObjectType(objectType)
|
||||
}
|
||||
} else {
|
||||
routingIdentifier = NewAuditRoutingIdentifier(objectId, objectType)
|
||||
}
|
||||
|
||||
auditLogEntry, err := builder.auditLogEntryBuilder.Build(ctx, sequenceNumber)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ctx, span := builder.tracer.Start(ctx, "create-audit-event")
|
||||
defer span.End()
|
||||
|
||||
w3cTraceParent := TraceParentFromSpan(span)
|
||||
var traceParent = &w3cTraceParent
|
||||
var traceState *string = nil
|
||||
visibility := builder.visibility
|
||||
|
||||
// Validate and serialize the protobuf event into a cloud event
|
||||
_, validateSerializeSpan := builder.tracer.Start(ctx, "validate-and-serialize-audit-event")
|
||||
cloudEvent, err := (*builder.api).ValidateAndSerializeWithTrace(auditLogEntry, visibility, routingIdentifier, traceParent, traceState)
|
||||
validateSerializeSpan.End()
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
builder.built = true
|
||||
return cloudEvent,
|
||||
routingIdentifier,
|
||||
nil
|
||||
}
|
||||
1167
audit/api/builder_test.go
Normal file
1167
audit/api/builder_test.go
Normal file
File diff suppressed because it is too large
Load diff
32
audit/api/converter.go
Normal file
32
audit/api/converter.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||
)
|
||||
|
||||
func StringToHttpMethod(method string) auditV1.AttributeContext_HttpMethod {
|
||||
switch method {
|
||||
case "GET":
|
||||
return auditV1.AttributeContext_HTTP_METHOD_GET
|
||||
case "HEAD":
|
||||
return auditV1.AttributeContext_HTTP_METHOD_HEAD
|
||||
case "POST":
|
||||
return auditV1.AttributeContext_HTTP_METHOD_POST
|
||||
case "PUT":
|
||||
return auditV1.AttributeContext_HTTP_METHOD_PUT
|
||||
case "DELETE":
|
||||
return auditV1.AttributeContext_HTTP_METHOD_DELETE
|
||||
case "CONNECT":
|
||||
return auditV1.AttributeContext_HTTP_METHOD_CONNECT
|
||||
case "OPTIONS":
|
||||
return auditV1.AttributeContext_HTTP_METHOD_OPTIONS
|
||||
case "TRACE":
|
||||
return auditV1.AttributeContext_HTTP_METHOD_TRACE
|
||||
case "PATCH":
|
||||
return auditV1.AttributeContext_HTTP_METHOD_PATCH
|
||||
case "OTHER":
|
||||
return auditV1.AttributeContext_HTTP_METHOD_OTHER
|
||||
default:
|
||||
return auditV1.AttributeContext_HTTP_METHOD_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
100
audit/api/log.go
Normal file
100
audit/api/log.go
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/log"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LogEvent logs an event to the terminal
|
||||
func LogEvent(event *CloudEvent) error {
|
||||
|
||||
if event.DataType == DataTypeLegacyAuditEventV1 {
|
||||
log.AuditLogger.Info(string(event.Data))
|
||||
return nil
|
||||
} else if event.DataType != "audit.v1.RoutableAuditEvent" {
|
||||
return errors.New("Unsupported data type " + event.DataType)
|
||||
}
|
||||
|
||||
var routableAuditEvent auditV1.RoutableAuditEvent
|
||||
err := proto.Unmarshal(event.Data, &routableAuditEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var auditEvent auditV1.AuditLogEntry
|
||||
err = proto.Unmarshal(routableAuditEvent.GetUnencryptedData().Data, &auditEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert to json
|
||||
auditEventJson, err := protojson.Marshal(&auditEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
auditEventMap := make(map[string]interface{})
|
||||
err = json.Unmarshal(auditEventJson, &auditEventMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
objectIdentifierJson, err := protojson.Marshal(routableAuditEvent.ObjectIdentifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
objectIdentifierMap := make(map[string]interface{})
|
||||
err = json.Unmarshal(objectIdentifierJson, &objectIdentifierMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cloudEvent := cloudEvent{
|
||||
SpecVersion: event.SpecVersion,
|
||||
Source: event.Source,
|
||||
Id: event.Id,
|
||||
Time: event.Time,
|
||||
DataContentType: event.DataContentType,
|
||||
DataType: event.DataType,
|
||||
Subject: event.Subject,
|
||||
Data: routableEvent{
|
||||
OperationName: auditEvent.ProtoPayload.OperationName,
|
||||
Visibility: routableAuditEvent.Visibility.String(),
|
||||
ResourceReference: objectIdentifierMap,
|
||||
Data: auditEventMap,
|
||||
},
|
||||
TraceParent: event.TraceParent,
|
||||
TraceState: event.TraceState,
|
||||
}
|
||||
cloudEventJson, err := json.Marshal(cloudEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.AuditLogger.Info(string(cloudEventJson))
|
||||
return nil
|
||||
}
|
||||
|
||||
type cloudEvent struct {
|
||||
SpecVersion string
|
||||
Source string
|
||||
Id string
|
||||
Time time.Time
|
||||
DataContentType string
|
||||
DataType string
|
||||
Subject string
|
||||
Data routableEvent
|
||||
TraceParent *string
|
||||
TraceState *string
|
||||
}
|
||||
|
||||
type routableEvent struct {
|
||||
OperationName string
|
||||
Visibility string
|
||||
ResourceReference map[string]interface{}
|
||||
Data map[string]interface{}
|
||||
}
|
||||
101
audit/api/log_test.go
Normal file
101
audit/api/log_test.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/utils"
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||
"github.com/bufbuild/protovalidate-go"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.opentelemetry.io/otel"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_LogEvent(t *testing.T) {
|
||||
|
||||
api, _ := NewMockAuditApi()
|
||||
sequenceNumberGenerator := utils.NewDefaultSequenceNumberGenerator()
|
||||
tracer := otel.Tracer("test-tracer")
|
||||
|
||||
t.Run("new format", func(t *testing.T) {
|
||||
eventBuilder := NewAuditEventBuilder(api, sequenceNumberGenerator, tracer, "demo-service", uuid.NewString(), "eu01")
|
||||
|
||||
cloudEvent, _, err := eventBuilder.
|
||||
WithRequiredApiRequest(ApiRequest{
|
||||
Body: nil,
|
||||
Header: TestHeaders,
|
||||
Host: "localhost",
|
||||
Method: "GET",
|
||||
Scheme: "https",
|
||||
Proto: "HTTP/1.1",
|
||||
URL: RequestUrl{
|
||||
Path: "/",
|
||||
RawQuery: nil,
|
||||
},
|
||||
}).
|
||||
WithRequiredObjectId(uuid.NewString()).
|
||||
WithRequiredObjectType(ObjectTypeProject).
|
||||
WithRequiredOperation("stackit.demo-service.v1.project.update").
|
||||
WithRequiredRequestClientIp("0.0.0.0").
|
||||
Build(context.Background(), eventBuilder.NextSequenceNumber())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, LogEvent(cloudEvent))
|
||||
})
|
||||
|
||||
t.Run("legacy format", func(t *testing.T) {
|
||||
objectId := uuid.NewString()
|
||||
entry, err := NewAuditLogEntryBuilder().
|
||||
WithRequiredApiRequest(ApiRequest{
|
||||
Body: nil,
|
||||
Header: TestHeaders,
|
||||
Host: "localhost",
|
||||
Method: "GET",
|
||||
Scheme: "https",
|
||||
Proto: "HTTP/1.1",
|
||||
URL: RequestUrl{
|
||||
Path: "/",
|
||||
RawQuery: nil,
|
||||
},
|
||||
}).
|
||||
WithRequiredLocation("eu01").
|
||||
WithRequiredObjectId(objectId).
|
||||
WithRequiredObjectType(ObjectTypeProject).
|
||||
WithRequiredOperation("stackit.demo-service.v1.project.update").
|
||||
WithRequiredRequestClientIp("0.0.0.0").
|
||||
WithRequiredServiceName("demo-service").
|
||||
WithRequiredWorkerId(uuid.NewString()).
|
||||
Build(context.Background(), SequenceNumber(1))
|
||||
assert.NoError(t, err)
|
||||
|
||||
validator, err := protovalidate.New()
|
||||
assert.NoError(t, err)
|
||||
var protoValidator ProtobufValidator = validator
|
||||
|
||||
routableIdentifier := RoutableIdentifier{
|
||||
Identifier: objectId,
|
||||
Type: ObjectTypeProject,
|
||||
}
|
||||
|
||||
routableEvent, err := validateAndSerializePartially(&protoValidator, entry, auditV1.Visibility_VISIBILITY_PUBLIC, &routableIdentifier)
|
||||
assert.NoError(t, err)
|
||||
|
||||
legacyBytes, err := convertAndSerializeIntoLegacyFormat(entry, routableEvent)
|
||||
assert.NoError(t, err)
|
||||
|
||||
cloudEvent := CloudEvent{
|
||||
SpecVersion: "1.0",
|
||||
Source: entry.ProtoPayload.ServiceName,
|
||||
Id: entry.InsertId,
|
||||
Time: entry.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(),
|
||||
DataContentType: ContentTypeCloudEventsJson,
|
||||
DataType: DataTypeLegacyAuditEventV1,
|
||||
Subject: entry.ProtoPayload.ResourceName,
|
||||
Data: legacyBytes,
|
||||
TraceParent: nil,
|
||||
TraceState: nil,
|
||||
}
|
||||
|
||||
assert.NoError(t, LogEvent(&cloudEvent))
|
||||
})
|
||||
}
|
||||
983
audit/api/model.go
Normal file
983
audit/api/model.go
Normal file
|
|
@ -0,0 +1,983 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrInvalidRequestBody = errors.New("invalid request body")
|
||||
var ErrInvalidResponse = errors.New("invalid response")
|
||||
var ErrInvalidAuthorizationHeaderValue = errors.New("invalid authorization header value")
|
||||
var ErrInvalidBearerToken = errors.New("invalid bearer token")
|
||||
var ErrTokenIsNotBearerToken = errors.New("token is not a bearer token")
|
||||
|
||||
var objectTypeIdPattern, _ = regexp.Compile(".*/(projects|folders|organizations)/([0-9a-fA-F-]{36})(?:/.*)?")
|
||||
|
||||
type ApiRequest struct {
|
||||
|
||||
// Body
|
||||
//
|
||||
// Required: false
|
||||
Body *[]byte
|
||||
|
||||
// The (HTTP) request headers / gRPC metadata.
|
||||
//
|
||||
// Internal IP-Addresses have to be removed (e.g. in x-forwarded-xxx headers).
|
||||
//
|
||||
// Required: true
|
||||
Header map[string][]string
|
||||
|
||||
// The HTTP request `Host` header value.
|
||||
//
|
||||
// Required: true
|
||||
Host string
|
||||
|
||||
// Method
|
||||
//
|
||||
// Required: true
|
||||
Method string
|
||||
|
||||
// The URL scheme, such as `http`, `https` or `gRPC`.
|
||||
//
|
||||
// Required: true
|
||||
Scheme string
|
||||
|
||||
// The network protocol used with the request, such as "http/1.1",
|
||||
// "spdy/3", "h2", "h2c", "webrtc", "tcp", "udp", "quic". See
|
||||
// https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
|
||||
// for details.
|
||||
//
|
||||
// Required: true
|
||||
Proto string
|
||||
|
||||
// The url
|
||||
//
|
||||
// Required: true
|
||||
URL RequestUrl
|
||||
}
|
||||
|
||||
type RequestUrl struct {
|
||||
|
||||
// The gRPC / HTTP URL path.
|
||||
//
|
||||
// Required: true
|
||||
Path string
|
||||
|
||||
// The HTTP URL query in the format of "name1=value1&name2=value2", as it
|
||||
// appears in the first line of the HTTP request.
|
||||
// The input should be escaped to not contain any special characters.
|
||||
//
|
||||
// Required: false
|
||||
RawQuery *string
|
||||
}
|
||||
|
||||
// AuditRequest bundles request related parameters
|
||||
type AuditRequest struct {
|
||||
|
||||
// The operation request. This may not include all request parameters,
|
||||
// such as those that are too large, privacy-sensitive, or duplicated
|
||||
// elsewhere in the log record.
|
||||
// It should never include user-generated data, such as file contents.
|
||||
//
|
||||
// Required: true
|
||||
Request *ApiRequest
|
||||
|
||||
// The IP address of the caller.
|
||||
// For caller from internet, this will be public IPv4 or IPv6 address.
|
||||
// For caller from a VM / K8s Service / etc., this will be the SIT proxy's IPv4 address.
|
||||
//
|
||||
// Required: true
|
||||
RequestClientIP string
|
||||
|
||||
// Correlate multiple audit logs by setting the same id
|
||||
//
|
||||
// Required: false
|
||||
RequestCorrelationId *string
|
||||
|
||||
// The unique ID for a request, which can be propagated to downstream
|
||||
// systems. The ID should have low probability of collision
|
||||
// within a single day for a specific service.
|
||||
//
|
||||
// More information can be found here: https://google.aip.dev/155
|
||||
//
|
||||
// Format: <idempotency-key>
|
||||
// Where:
|
||||
// Idempotency-key: Typically consists of an id + version
|
||||
//
|
||||
// Examples:
|
||||
// 5e3952a9-b628-4be6-ac61-b1c6eb4a110c/5
|
||||
//
|
||||
// Required: false
|
||||
RequestId *string
|
||||
|
||||
// The timestamp when the `destination` service receives the first byte of
|
||||
// the request.
|
||||
//
|
||||
// Required: false
|
||||
RequestTime *time.Time
|
||||
}
|
||||
|
||||
// AuditResponse bundles response related parameters
|
||||
type AuditResponse struct {
|
||||
|
||||
// The operation response. This may not include all response elements,
|
||||
// such as those that are too large, privacy-sensitive, or duplicated
|
||||
// elsewhere in the log record.
|
||||
//
|
||||
// Required: false
|
||||
ResponseBodyBytes *[]byte
|
||||
|
||||
// The http or gRPC status code.
|
||||
//
|
||||
// Examples:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
|
||||
// https://grpc.github.io/grpc/core/md_doc_statuscodes.html
|
||||
//
|
||||
// Required: true
|
||||
ResponseStatusCode int
|
||||
|
||||
// The HTTP response headers.
|
||||
//
|
||||
// Required: true
|
||||
ResponseHeaders map[string][]string
|
||||
|
||||
// The number of items returned from a List or Query API method,
|
||||
// if applicable.
|
||||
//
|
||||
// Required: false
|
||||
ResponseNumItems *int64
|
||||
|
||||
// The timestamp when the "destination" service generates the first byte of
|
||||
// the response.
|
||||
//
|
||||
// Required: false
|
||||
ResponseTime *time.Time
|
||||
}
|
||||
|
||||
// AuditMetadata bundles audit event related metadata
|
||||
type AuditMetadata struct {
|
||||
|
||||
// A unique identifier for the log entry.
|
||||
// Is used to check completeness of audit events over time.
|
||||
//
|
||||
// Format: <unix-timestamp>/<region-zone>/<worker-id>/<sequence-number>
|
||||
// Where:
|
||||
// Unix-Timestamp: A UTC unix timestamp in seconds is expected
|
||||
// Region-Zone: The region and (optional) zone id. If both, separated with a - (dash)
|
||||
// Worker-Id: The ID of the K8s Pod, Service-Instance, etc. (must be unique for a sending service)
|
||||
// Sequence-Number: Increasing number, representing the message offset per Worker-Id
|
||||
// If the Worker-Id changes, the sequence-number has to be reset to 0.
|
||||
//
|
||||
// Examples:
|
||||
// "1721899117/eu01/319a7fb9-edd2-46c6-953a-a724bb377c61/8792726390909855142"
|
||||
//
|
||||
// Required: true
|
||||
AuditInsertId string
|
||||
|
||||
// A set of user-defined (key, value) data that provides additional
|
||||
// information about the log entry.
|
||||
//
|
||||
// Required: false
|
||||
AuditLabels *map[string]string
|
||||
|
||||
// The resource name of the log to which this log entry belongs.
|
||||
//
|
||||
// Format: <pluralType>/<identifier>/logs/<eventType>
|
||||
// Where:
|
||||
// Plural-Types: One from the list of supported ObjectType as plural
|
||||
// Event-Types: admin-activity, system-event, policy-denied, data-access
|
||||
//
|
||||
// Examples:
|
||||
// "projects/00b0f972-59ff-48f2-a4f9-29c57b75c2fa/logs/admin-activity"
|
||||
//
|
||||
// Required: true
|
||||
AuditLogName string
|
||||
|
||||
// The severity of the log entry.
|
||||
//
|
||||
// Required: true
|
||||
AuditLogSeverity auditV1.LogSeverity
|
||||
|
||||
// The name of the service method or operation.
|
||||
//
|
||||
// Format: stackit.<product>.<version>.<type-chain>.<operation>
|
||||
// Where:
|
||||
// Product: The name of the service in lowercase
|
||||
// Version: Optional API version
|
||||
// Type-Chain: Optional chained path to object
|
||||
// Operation: The name of the operation in lowercase
|
||||
//
|
||||
// Examples:
|
||||
// "stackit.resource-manager.v1.organizations.create"
|
||||
// "stackit.authorization.v1.projects.volumes.create"
|
||||
// "stackit.authorization.v2alpha.projects.volumes.create"
|
||||
// "stackit.authorization.v2.folders.move"
|
||||
// "stackit.resource-manager.health"
|
||||
//
|
||||
// Required: true
|
||||
AuditOperationName string
|
||||
|
||||
// The required IAM permission.
|
||||
//
|
||||
// Examples:
|
||||
// "resourcemanager.project.edit"
|
||||
//
|
||||
// Required: false
|
||||
AuditPermission *string
|
||||
|
||||
// Result of the IAM permission check.
|
||||
//
|
||||
// Required: false
|
||||
AuditPermissionGranted *bool
|
||||
|
||||
// The resource or collection that is the target of the operation.
|
||||
// The name is a scheme-less URI, not including the API service name.
|
||||
//
|
||||
// Format: <pluralType>/<id>[/locations/<region-zone>][/<details>]
|
||||
// Where:
|
||||
// Plural-Type: One from the list of supported ObjectType as plural
|
||||
// Id: The identifier of the object
|
||||
// Region-Zone: Optional region and zone id. If both, separated with a - (dash). Alternatively _ (underscore).
|
||||
// Details: Optional "<key>/<id>" pairs
|
||||
//
|
||||
// Examples:
|
||||
// "organizations/40ab14ad-b7b0-4b1c-be41-5bc820a968d1"
|
||||
// "projects/7046e7b6-5ae9-441c-99fe-2cd28a5078ec/locations/_/instances/instance-20240723-174217"
|
||||
// "projects/7046e7b6-5ae9-441c-99fe-2cd28a5078ec/locations/eu01/instances/instance-20240723-174217"
|
||||
// "projects/7046e7b6-5ae9-441c-99fe-2cd28a5078ec/locations/sx-stoi01/instances/instance-20240723-174217"
|
||||
// "projects/dd7d1807-54e9-4426-8994-721758b5b554/locations/eu01-m/vms/b6851b4e-7a9d-4973-ab0f-a80a13ee3060/ports/78f8bad4-a291-4fa3-b07f-4a1985d3dbe8"
|
||||
//
|
||||
// Required: true
|
||||
AuditResourceName string
|
||||
|
||||
// The name of the API service performing the operation.
|
||||
//
|
||||
// Examples:
|
||||
// "resource-manager"
|
||||
//
|
||||
// Required: true
|
||||
AuditServiceName string
|
||||
|
||||
// The time the event described by the log entry occurred.
|
||||
//
|
||||
// Required: false
|
||||
AuditTime *time.Time
|
||||
}
|
||||
|
||||
// NewAuditLogEntry constructs a new audit log event for the given parameters
|
||||
func NewAuditLogEntry(
|
||||
|
||||
// Required request parameters
|
||||
auditRequest AuditRequest,
|
||||
|
||||
// Required response parameters
|
||||
auditResponse AuditResponse,
|
||||
|
||||
// Optional map that is added as "details" to the message
|
||||
eventMetadata *map[string]interface{},
|
||||
|
||||
// Required metadata
|
||||
auditMetadata AuditMetadata,
|
||||
|
||||
// Optional W3C trace parent
|
||||
userProvidedTraceParent *string,
|
||||
|
||||
// Optional W3C trace state
|
||||
userProvidedTraceState *string,
|
||||
) (*auditV1.AuditLogEntry, error) {
|
||||
|
||||
// Get request headers
|
||||
filteredRequestHeaders := FilterAndMergeHeaders(auditRequest.Request.Header)
|
||||
filteredResponseHeaders := FilterAndMergeHeaders(auditResponse.ResponseHeaders)
|
||||
|
||||
// Get response body
|
||||
responseBody, err := NewResponseBody(auditResponse.ResponseBodyBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Join(err, ErrInvalidResponse)
|
||||
}
|
||||
var responseLength *int64 = nil
|
||||
if responseBody != nil {
|
||||
length := int64(len(*auditResponse.ResponseBodyBytes))
|
||||
responseLength = &length
|
||||
}
|
||||
|
||||
// Get request body
|
||||
requestBody, err := NewRequestBody(auditRequest.Request)
|
||||
if err != nil {
|
||||
return nil, errors.Join(err, ErrInvalidRequestBody)
|
||||
}
|
||||
|
||||
// Get audit attributes from request
|
||||
auditClaims, authenticationPrincipal, audiences, authenticationInfo, err :=
|
||||
AuditAttributesFromAuthorizationHeader(auditRequest.Request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get request scheme (http, https)
|
||||
scheme := auditRequest.Request.Scheme
|
||||
|
||||
// Initialize authorization info if available
|
||||
var authorizationInfo []*auditV1.AuthorizationInfo = nil
|
||||
if auditMetadata.AuditPermission != nil && auditMetadata.AuditPermissionGranted != nil {
|
||||
authorizationInfo = []*auditV1.AuthorizationInfo{
|
||||
NewAuthorizationInfo(
|
||||
auditMetadata.AuditResourceName,
|
||||
*auditMetadata.AuditPermission,
|
||||
*auditMetadata.AuditPermissionGranted)}
|
||||
}
|
||||
|
||||
// Initialize labels if available
|
||||
var labels map[string]string = nil
|
||||
if auditMetadata.AuditLabels != nil {
|
||||
labels = *auditMetadata.AuditLabels
|
||||
}
|
||||
|
||||
// Initialize metadata/details
|
||||
var metadata *structpb.Struct = nil
|
||||
if eventMetadata != nil {
|
||||
metadataStruct, err := structpb.NewStruct(*eventMetadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metadata = metadataStruct
|
||||
}
|
||||
|
||||
// Get request and audit time
|
||||
var concreteRequestTime = time.Now().UTC()
|
||||
if auditRequest.RequestTime != nil {
|
||||
concreteRequestTime = *auditRequest.RequestTime
|
||||
}
|
||||
var concreteAuditTime = concreteRequestTime
|
||||
if auditMetadata.AuditTime != nil {
|
||||
concreteAuditTime = *auditMetadata.AuditTime
|
||||
}
|
||||
var concreteResponseTime = concreteRequestTime
|
||||
if auditResponse.ResponseTime != nil {
|
||||
concreteResponseTime = *auditResponse.ResponseTime
|
||||
}
|
||||
|
||||
// Initialize the audit log entry
|
||||
event := auditV1.AuditLogEntry{
|
||||
LogName: auditMetadata.AuditLogName,
|
||||
ProtoPayload: &auditV1.AuditLog{
|
||||
ServiceName: auditMetadata.AuditServiceName,
|
||||
OperationName: auditMetadata.AuditOperationName,
|
||||
ResourceName: auditMetadata.AuditResourceName,
|
||||
AuthenticationInfo: authenticationInfo,
|
||||
AuthorizationInfo: authorizationInfo,
|
||||
RequestMetadata: NewRequestMetadata(
|
||||
auditRequest.Request,
|
||||
filteredRequestHeaders,
|
||||
auditRequest.RequestId,
|
||||
scheme,
|
||||
concreteRequestTime,
|
||||
auditRequest.RequestClientIP,
|
||||
authenticationPrincipal,
|
||||
audiences,
|
||||
auditClaims),
|
||||
Request: requestBody,
|
||||
ResponseMetadata: NewResponseMetadata(
|
||||
auditResponse.ResponseStatusCode,
|
||||
auditResponse.ResponseNumItems,
|
||||
responseLength,
|
||||
filteredResponseHeaders,
|
||||
concreteResponseTime),
|
||||
Response: responseBody,
|
||||
Metadata: metadata,
|
||||
},
|
||||
InsertId: auditMetadata.AuditInsertId,
|
||||
Labels: labels,
|
||||
CorrelationId: auditRequest.RequestCorrelationId,
|
||||
Timestamp: timestamppb.New(concreteAuditTime),
|
||||
Severity: auditMetadata.AuditLogSeverity,
|
||||
TraceParent: userProvidedTraceParent,
|
||||
TraceState: userProvidedTraceState,
|
||||
}
|
||||
return &event, nil
|
||||
}
|
||||
|
||||
// GetCalledServiceNameFromRequest extracts the called service name from subdomain name
|
||||
func GetCalledServiceNameFromRequest(request *ApiRequest, fallbackName string) string {
|
||||
if request == nil {
|
||||
return fallbackName
|
||||
}
|
||||
|
||||
var calledServiceName = fallbackName
|
||||
host := request.Host
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil && !strings.Contains(host, "localhost") {
|
||||
dotIdx := strings.Index(host, ".")
|
||||
if dotIdx != -1 {
|
||||
calledServiceName = host[0:dotIdx]
|
||||
}
|
||||
}
|
||||
return calledServiceName
|
||||
}
|
||||
|
||||
// AuditSpan is an abstraction for trace.Span that can easier be tested
|
||||
type AuditSpan interface {
|
||||
SpanContext() trace.SpanContext
|
||||
}
|
||||
|
||||
// TraceParentFromSpan returns W3C conform trace parent from AuditSpan
|
||||
func TraceParentFromSpan(span AuditSpan) string {
|
||||
traceVersion := "00"
|
||||
traceId := span.SpanContext().TraceID().String()
|
||||
parentId := span.SpanContext().SpanID().String()
|
||||
// Trace flags according to W3C documentation:
|
||||
// https://www.w3.org/TR/trace-context/#sampled-flag
|
||||
var traceFlags = "00"
|
||||
if span.SpanContext().TraceFlags().IsSampled() {
|
||||
traceFlags = "01"
|
||||
}
|
||||
|
||||
// Format: <version>-<trace-id>-<parent-id>-<trace-flags>
|
||||
// Example: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
|
||||
w3cTraceParent := fmt.Sprintf("%s-%s-%s-%s", traceVersion, traceId, parentId, traceFlags)
|
||||
return w3cTraceParent
|
||||
}
|
||||
|
||||
// NewPbInt64Value returns protobuf int64 wrapper if value is not nil.
|
||||
func NewPbInt64Value(value *int64) *wrapperspb.Int64Value {
|
||||
if value != nil {
|
||||
return wrapperspb.Int64(*value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewRequestMetadata returns initialized protobuf RequestMetadata object.
|
||||
func NewRequestMetadata(
|
||||
request *ApiRequest,
|
||||
requestHeaders map[string]string,
|
||||
requestId *string,
|
||||
requestScheme string,
|
||||
requestTime time.Time,
|
||||
clientIp string,
|
||||
authenticationPrincipal string,
|
||||
audiences []string,
|
||||
auditClaims *structpb.Struct,
|
||||
) *auditV1.RequestMetadata {
|
||||
agent := requestHeaders["User-Agent"]
|
||||
if agent == "" {
|
||||
agent = requestHeaders["user-agent"]
|
||||
}
|
||||
return &auditV1.RequestMetadata{
|
||||
CallerIp: clientIp,
|
||||
CallerSuppliedUserAgent: agent,
|
||||
RequestAttributes: NewRequestAttributes(
|
||||
request,
|
||||
requestHeaders,
|
||||
requestId,
|
||||
requestScheme,
|
||||
requestTime,
|
||||
authenticationPrincipal,
|
||||
audiences,
|
||||
auditClaims,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// NewRequestAttributes returns initialized protobuf AttributeContext_Request object.
|
||||
func NewRequestAttributes(
|
||||
request *ApiRequest,
|
||||
requestHeaders map[string]string,
|
||||
requestId *string,
|
||||
requestScheme string,
|
||||
requestTime time.Time,
|
||||
authenticationPrincipal string,
|
||||
audiences []string,
|
||||
auditClaims *structpb.Struct,
|
||||
) *auditV1.AttributeContext_Request {
|
||||
|
||||
rawQuery := request.URL.RawQuery
|
||||
var query *string = nil
|
||||
if rawQuery != nil && *rawQuery != "" {
|
||||
escapedQuery := url.QueryEscape(*rawQuery)
|
||||
query = &escapedQuery
|
||||
}
|
||||
|
||||
return &auditV1.AttributeContext_Request{
|
||||
Id: requestId,
|
||||
Method: StringToHttpMethod(request.Method),
|
||||
Headers: requestHeaders,
|
||||
Path: request.URL.Path,
|
||||
Host: request.Host,
|
||||
Scheme: requestScheme,
|
||||
Query: query,
|
||||
Time: timestamppb.New(requestTime),
|
||||
Protocol: request.Proto,
|
||||
Auth: &auditV1.AttributeContext_Auth{
|
||||
Principal: authenticationPrincipal,
|
||||
Audiences: audiences,
|
||||
Claims: auditClaims,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewAuthorizationInfo returns protobuf AuthorizationInfo for the given parameters.
|
||||
func NewAuthorizationInfo(resourceName string, permission string, granted bool) *auditV1.AuthorizationInfo {
|
||||
return &auditV1.AuthorizationInfo{
|
||||
Resource: resourceName,
|
||||
Permission: &permission,
|
||||
Granted: &granted,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInsertId returns a correctly formatted insert id.
|
||||
func NewInsertId(insertTime time.Time, location string, workerId string, eventSequenceNumber uint64) string {
|
||||
return fmt.Sprintf("%d/%s/%s/%d", insertTime.UnixNano(), location, workerId, eventSequenceNumber)
|
||||
}
|
||||
|
||||
// NewResponseMetadata returns protobuf response status with status code and short message.
|
||||
func NewResponseMetadata(statusCode int, numResponseItems *int64, responseSize *int64, headers map[string]string, responseTime time.Time) *auditV1.ResponseMetadata {
|
||||
|
||||
var message *string = nil
|
||||
if statusCode >= 400 && statusCode < 500 {
|
||||
text := "Client error"
|
||||
message = &text
|
||||
} else if statusCode >= 500 {
|
||||
text := "Server error"
|
||||
message = &text
|
||||
}
|
||||
|
||||
var size *wrapperspb.Int64Value = nil
|
||||
if responseSize != nil {
|
||||
size = wrapperspb.Int64(*responseSize)
|
||||
}
|
||||
return &auditV1.ResponseMetadata{
|
||||
StatusCode: wrapperspb.Int32(int32(statusCode)),
|
||||
ErrorMessage: message,
|
||||
ErrorDetails: nil,
|
||||
ResponseAttributes: &auditV1.AttributeContext_Response{
|
||||
NumResponseItems: NewPbInt64Value(numResponseItems),
|
||||
Size: size,
|
||||
Headers: headers,
|
||||
Time: timestamppb.New(responseTime),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewResponseBody converts the JSON byte response into a protobuf struct.
|
||||
func NewResponseBody(response *[]byte) (*structpb.Struct, error) {
|
||||
|
||||
// Return if nil
|
||||
if response == nil || len(*response) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Convert to protobuf struct
|
||||
return byteArrayToPbStruct(*response)
|
||||
}
|
||||
|
||||
// NewRequestBody converts the request body into a protobuf struct.
|
||||
func NewRequestBody(request *ApiRequest) (*structpb.Struct, error) {
|
||||
|
||||
if request.Body == nil || len(*request.Body) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Convert to protobuf struct
|
||||
return byteArrayToPbStruct(*request.Body)
|
||||
}
|
||||
|
||||
// byteArrayToPbStruct converts a given json byte array into a protobuf struct.
|
||||
func byteArrayToPbStruct(bytes []byte) (*structpb.Struct, error) {
|
||||
var bodyMap map[string]interface{}
|
||||
err := json.Unmarshal(bytes, &bodyMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return structpb.NewStruct(bodyMap)
|
||||
}
|
||||
|
||||
// FilterAndMergeHeaders filters ":authority", "Authorization" and "B3" headers as well as
|
||||
// all headers starting with the prefixes "X-" and "STACKIT-".
|
||||
// Headers are merged if there is more than one value for a given name.
|
||||
func FilterAndMergeHeaders(headers map[string][]string) map[string]string {
|
||||
var resultMap = make(map[string]string)
|
||||
skipHeaders := []string{":authority", "authorization", "b3"}
|
||||
skipPrefixHeaders := []string{"x-", "stackit-"}
|
||||
|
||||
if len(headers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for headerName, headerValues := range headers {
|
||||
headerLower := strings.ToLower(headerName)
|
||||
|
||||
// Check if headers with a specific prefix is found
|
||||
skip := false
|
||||
for _, skipPrefix := range skipPrefixHeaders {
|
||||
if strings.HasPrefix(headerLower, skipPrefix) {
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Keep header if not on filter list or value is empty
|
||||
if !skip && !slices.Contains(skipHeaders, headerLower) && len(headerValues) > 0 {
|
||||
resultMap[headerName] = strings.Join(headerValues, ",")
|
||||
}
|
||||
}
|
||||
|
||||
return resultMap
|
||||
}
|
||||
|
||||
// NewAuditRoutingIdentifier instantiates a new auditApi.RoutableIdentifier for
|
||||
// the given object ID and object type.
|
||||
func NewAuditRoutingIdentifier(objectId string, objectType ObjectType) *RoutableIdentifier {
|
||||
return &RoutableIdentifier{
|
||||
Identifier: objectId,
|
||||
Type: objectType,
|
||||
}
|
||||
}
|
||||
|
||||
// AuditAttributesFromAuthorizationHeader extracts the following claims from given http.Request:
|
||||
// - auditClaims - filtered list of claims
|
||||
// - authenticationPrincipal - principal identifier
|
||||
// - audiences - list of audience claims
|
||||
// - authenticationInfo - information about the user or service-account authentication
|
||||
func AuditAttributesFromAuthorizationHeader(request *ApiRequest) (
|
||||
*structpb.Struct,
|
||||
string,
|
||||
[]string,
|
||||
*auditV1.AuthenticationInfo,
|
||||
error,
|
||||
) {
|
||||
|
||||
var principalId = "none"
|
||||
var principalEmail = "do-not-reply@stackit.cloud"
|
||||
emptyClaims, _ := structpb.NewStruct(make(map[string]interface{}))
|
||||
var auditClaims = emptyClaims
|
||||
var authenticationPrincipal = "none/none"
|
||||
var serviceAccountName *string = nil
|
||||
audiences := make([]string, 0)
|
||||
var delegationInfo []*auditV1.ServiceAccountDelegationInfo = nil
|
||||
|
||||
authorizationHeaders := request.Header["Authorization"]
|
||||
if len(authorizationHeaders) == 0 {
|
||||
// fallback for grpc where headers/metadata keys are lowercase
|
||||
authorizationHeaders = request.Header["authorization"]
|
||||
}
|
||||
authorizationHeader := strings.Join(authorizationHeaders, ",")
|
||||
trimmedAuthorizationHeader := strings.TrimSpace(authorizationHeader)
|
||||
if len(trimmedAuthorizationHeader) > 0 {
|
||||
|
||||
// Parse claims
|
||||
parsedClaims, filteredClaims, err := parseClaimsFromAuthorizationHeader(trimmedAuthorizationHeader)
|
||||
if err != nil {
|
||||
return nil, authenticationPrincipal, nil, nil, err
|
||||
}
|
||||
|
||||
// Convert filtered claims to protobuf struct
|
||||
auditClaimsStruct, err := structpb.NewStruct(filteredClaims)
|
||||
if err != nil {
|
||||
return nil, authenticationPrincipal, nil, nil, err
|
||||
}
|
||||
auditClaims = auditClaimsStruct
|
||||
|
||||
// Extract principal data
|
||||
authenticationPrincipal = extractAuthenticationPrincipal(parsedClaims)
|
||||
principalId, principalEmail = extractSubjectAndEmail(parsedClaims)
|
||||
|
||||
// Extract service account delegation info data
|
||||
delegationInfo = extractServiceAccountDelegationInfo(parsedClaims)
|
||||
|
||||
// Extract audiences data
|
||||
audiences, err = extractAudiences(parsedClaims)
|
||||
if err != nil {
|
||||
return nil, authenticationPrincipal, nil, nil, err
|
||||
}
|
||||
|
||||
// Extract project id and service account id
|
||||
projectId := extractServiceAccountProjectId(parsedClaims)
|
||||
serviceAccountId := extractServiceAccountId(parsedClaims)
|
||||
|
||||
// Calculate service account name if project id and service account id are available
|
||||
if projectId != nil && serviceAccountId != nil {
|
||||
accountName := fmt.Sprintf("projects/%s/service-accounts/%s", *projectId, *serviceAccountId)
|
||||
serviceAccountName = &accountName
|
||||
}
|
||||
}
|
||||
|
||||
authenticationInfo := auditV1.AuthenticationInfo{
|
||||
PrincipalId: principalId,
|
||||
PrincipalEmail: principalEmail,
|
||||
ServiceAccountName: serviceAccountName,
|
||||
ServiceAccountDelegationInfo: delegationInfo,
|
||||
}
|
||||
|
||||
return auditClaims, authenticationPrincipal, audiences, &authenticationInfo, nil
|
||||
}
|
||||
|
||||
func extractServiceAccountId(parsedClaims map[string]interface{}) *string {
|
||||
projectId, projectIdExists := parsedClaims["stackit/serviceaccount/service-account.uid"]
|
||||
if projectIdExists {
|
||||
projectIdString := fmt.Sprintf("%s", projectId)
|
||||
return &projectIdString
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func extractServiceAccountProjectId(parsedClaims map[string]interface{}) *string {
|
||||
projectId, projectIdExists := parsedClaims["stackit/project/project.id"]
|
||||
if projectIdExists {
|
||||
projectIdString := fmt.Sprintf("%s", projectId)
|
||||
return &projectIdString
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func extractAudiences(parsedClaims map[string]interface{}) ([]string, error) {
|
||||
audClaim, _ := json.Marshal(parsedClaims["aud"])
|
||||
|
||||
var audiences []string
|
||||
if err := json.Unmarshal(audClaim, &audiences); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return audiences, nil
|
||||
}
|
||||
|
||||
func extractAuthenticationPrincipal(parsedClaims map[string]interface{}) string {
|
||||
subClaim, subExists := parsedClaims["sub"]
|
||||
issuerClaim, issuerExists := parsedClaims["iss"]
|
||||
|
||||
var principal = "none/none"
|
||||
if subExists && issuerExists {
|
||||
principal = fmt.Sprintf("%s/%s", url.QueryEscape(subClaim.(string)), url.QueryEscape(issuerClaim.(string)))
|
||||
}
|
||||
return principal
|
||||
}
|
||||
|
||||
func parseClaimsFromAuthorizationHeader(authorizationHeader string) (map[string]interface{}, map[string]interface{}, error) {
|
||||
parts := strings.Split(authorizationHeader, " ")
|
||||
if len(parts) != 2 {
|
||||
return nil, nil, ErrInvalidAuthorizationHeaderValue
|
||||
}
|
||||
if !strings.EqualFold(parts[0], "Bearer") {
|
||||
return nil, nil, ErrTokenIsNotBearerToken
|
||||
}
|
||||
jwt := parts[1]
|
||||
authorizationHeaderParts := strings.Split(jwt, ".")
|
||||
|
||||
parsedClaims := make(map[string]interface{})
|
||||
if len(authorizationHeaderParts) == 3 {
|
||||
// base64 decoding
|
||||
decodedString, err := base64.RawURLEncoding.DecodeString(authorizationHeaderParts[1])
|
||||
if err != nil {
|
||||
return parsedClaims, nil, errors.Join(err, ErrInvalidBearerToken)
|
||||
}
|
||||
|
||||
// unmarshall claim part of token
|
||||
err = json.Unmarshal(decodedString, &parsedClaims)
|
||||
if err != nil {
|
||||
return parsedClaims, nil, err
|
||||
}
|
||||
|
||||
// Collect user-friendly filtered subset of claims
|
||||
filteredClaims := make(map[string]interface{})
|
||||
_ = json.Unmarshal(decodedString, &filteredClaims)
|
||||
keysToDelete := make([]string, 0)
|
||||
for key := range filteredClaims {
|
||||
if key != "aud" && key != "email" && key != "iss" && key != "jti" && key != "sub" {
|
||||
keysToDelete = append(keysToDelete, key)
|
||||
}
|
||||
}
|
||||
for _, key := range keysToDelete {
|
||||
delete(filteredClaims, key)
|
||||
}
|
||||
return parsedClaims, filteredClaims, nil
|
||||
}
|
||||
return parsedClaims, nil, ErrInvalidBearerToken
|
||||
}
|
||||
|
||||
func extractServiceAccountDelegationInfoDetails(token map[string]interface{}) []*auditV1.ServiceAccountDelegationInfo {
|
||||
principalId, principalEmail := extractSubjectAndEmail(token)
|
||||
|
||||
delegation := auditV1.ServiceAccountDelegationInfo{Authority: &auditV1.ServiceAccountDelegationInfo_IdpPrincipal_{IdpPrincipal: &auditV1.ServiceAccountDelegationInfo_IdpPrincipal{
|
||||
PrincipalId: principalId,
|
||||
PrincipalEmail: principalEmail,
|
||||
ServiceMetadata: nil,
|
||||
}}}
|
||||
|
||||
delegations := []*auditV1.ServiceAccountDelegationInfo{&delegation}
|
||||
nestedDelegations := extractServiceAccountDelegationInfo(token)
|
||||
if len(nestedDelegations) > 0 {
|
||||
return append(delegations, nestedDelegations...)
|
||||
} else {
|
||||
return delegations
|
||||
}
|
||||
}
|
||||
|
||||
func extractServiceAccountDelegationInfo(token map[string]interface{}) []*auditV1.ServiceAccountDelegationInfo {
|
||||
actor := token["act"]
|
||||
if actor != nil {
|
||||
actorMap, hasActorClaim := actor.(map[string]interface{})
|
||||
if hasActorClaim {
|
||||
return extractServiceAccountDelegationInfoDetails(actorMap)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractSubjectAndEmail(token map[string]interface{}) (string, string) {
|
||||
var principalEmail string
|
||||
principalId := fmt.Sprintf("%s", token["sub"])
|
||||
principalEmailRaw := token["email"]
|
||||
if principalEmailRaw == nil {
|
||||
principalEmail = "do-not-reply@stackit.cloud"
|
||||
} else {
|
||||
principalEmail = fmt.Sprintf("%s", principalEmailRaw)
|
||||
}
|
||||
return principalId, principalEmail
|
||||
}
|
||||
|
||||
// OperationNameFromUrlPath converts the request url path into an operation name.
|
||||
// UUIDs and query parameters are filtered out, slashes replaced by dots.
|
||||
// HTTP methods are added as suffix as follows:
|
||||
// - POST - create
|
||||
// - PUT - update
|
||||
// - PATCH - update
|
||||
// - DELETE - delete
|
||||
// - others - read
|
||||
func OperationNameFromUrlPath(path string, requestMethod string) string {
|
||||
queryIdx := strings.Index(path, "?")
|
||||
if queryIdx != -1 {
|
||||
path = path[:queryIdx]
|
||||
}
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
split := strings.Split(path, "/")
|
||||
|
||||
operation := ""
|
||||
for _, part := range split {
|
||||
// skip uuids in path
|
||||
_, err := uuid.Parse(part)
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
operation = fmt.Sprintf("%s/%s", operation, part)
|
||||
}
|
||||
|
||||
operation = strings.ReplaceAll(operation, "/", ".")
|
||||
operation = strings.TrimPrefix(operation, ".")
|
||||
operation = strings.ToLower(operation)
|
||||
if len(operation) > 0 {
|
||||
method := StringToHttpMethod(requestMethod)
|
||||
var action string
|
||||
switch method {
|
||||
case auditV1.AttributeContext_HTTP_METHOD_PUT:
|
||||
fallthrough
|
||||
case auditV1.AttributeContext_HTTP_METHOD_PATCH:
|
||||
action = "update"
|
||||
case auditV1.AttributeContext_HTTP_METHOD_POST:
|
||||
action = "create"
|
||||
case auditV1.AttributeContext_HTTP_METHOD_DELETE:
|
||||
action = "delete"
|
||||
default:
|
||||
action = "read"
|
||||
}
|
||||
operation = fmt.Sprintf("%s.%s", operation, action)
|
||||
}
|
||||
|
||||
return operation
|
||||
}
|
||||
|
||||
// OperationNameFromGrpcMethod converts the grpc path into an operation name.
|
||||
func OperationNameFromGrpcMethod(path string) string {
|
||||
operation := strings.TrimPrefix(path, "/")
|
||||
operation = strings.TrimSuffix(operation, "/")
|
||||
|
||||
operation = strings.ReplaceAll(operation, "/", ".")
|
||||
operation = strings.TrimPrefix(operation, ".")
|
||||
operation = strings.ToLower(operation)
|
||||
|
||||
return operation
|
||||
}
|
||||
|
||||
func GetObjectIdAndTypeFromUrlPath(path string) (
|
||||
string,
|
||||
*ObjectType,
|
||||
error,
|
||||
) {
|
||||
|
||||
// Extract object id and type from request url
|
||||
objectTypeIdMatches := objectTypeIdPattern.FindStringSubmatch(path)
|
||||
if len(objectTypeIdMatches) > 0 {
|
||||
objectType := ObjectTypeFromPluralString(objectTypeIdMatches[1])
|
||||
err := objectType.IsSupportedType()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
objectId := objectTypeIdMatches[2]
|
||||
|
||||
return objectId, &objectType, nil
|
||||
}
|
||||
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
func ToArrayMap(input map[string]string) map[string][]string {
|
||||
output := map[string][]string{}
|
||||
for key, value := range input {
|
||||
output[key] = []string{value}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func StringAttributeFromMetadata(metadata map[string][]string, name string) string {
|
||||
var value = ""
|
||||
rawValue, hasAttribute := metadata[name]
|
||||
if hasAttribute && len(rawValue) > 0 {
|
||||
value = rawValue[0]
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// ResponseBodyToBytes converts a JSON or Protobuf response into a byte array
|
||||
func ResponseBodyToBytes(response any) (*[]byte, error) {
|
||||
if response == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
responseBytes, isBytes := response.([]byte)
|
||||
if isBytes {
|
||||
return &responseBytes, nil
|
||||
}
|
||||
|
||||
responseProtoMessage, isProtoMessage := response.(proto.Message)
|
||||
if isProtoMessage {
|
||||
responseJson, err := protojson.Marshal(responseProtoMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &responseJson, nil
|
||||
} else {
|
||||
responseJson, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &responseJson, nil
|
||||
}
|
||||
}
|
||||
1121
audit/api/model_test.go
Normal file
1121
audit/api/model_test.go
Normal file
File diff suppressed because it is too large
Load diff
128
audit/api/schema_validation_test.go
Normal file
128
audit/api/schema_validation_test.go
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||
"github.com/bufbuild/protovalidate-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_RoutableAuditEvent(t *testing.T) {
|
||||
|
||||
validator, err := protovalidate.New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
newEvent := func() auditV1.RoutableAuditEvent {
|
||||
return auditV1.RoutableAuditEvent{
|
||||
OperationName: "stackit.resource-manager.v1.organizations.create",
|
||||
Visibility: auditV1.Visibility_VISIBILITY_PUBLIC,
|
||||
ObjectIdentifier: &auditV1.ObjectIdentifier{
|
||||
Identifier: "14f7aa86-77ba-4d77-a091-a2cf3395a221",
|
||||
Type: string(ObjectTypeProject),
|
||||
},
|
||||
Data: &auditV1.RoutableAuditEvent_UnencryptedData{UnencryptedData: &auditV1.UnencryptedData{
|
||||
Data: []byte("data"),
|
||||
ProtobufType: "audit.v1.AuditLogEntry",
|
||||
}}}
|
||||
}
|
||||
|
||||
t.Run("valid event", func(t *testing.T) {
|
||||
event := newEvent()
|
||||
err := validator.Validate(&event)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("empty operation name", func(t *testing.T) {
|
||||
event := newEvent()
|
||||
event.OperationName = ""
|
||||
|
||||
err := validator.Validate(&event)
|
||||
assert.EqualError(t, err, "validation error:\n - operation_name: value is required [required]")
|
||||
})
|
||||
|
||||
t.Run("invalid operation name", func(t *testing.T) {
|
||||
event := newEvent()
|
||||
event.OperationName = "stackit.resource-manager.v1.INVALID.organizations.create"
|
||||
|
||||
err := validator.Validate(&event)
|
||||
assert.EqualError(t, err, "validation error:\n - operation_name: value does not match regex pattern `^stackit\\.[a-z0-9-]+\\.(?:v[0-9]+\\.)?(?:[a-z0-9-.]+\\.)?[a-z0-9-]+$` [string.pattern]")
|
||||
})
|
||||
|
||||
t.Run("visibility invalid", func(t *testing.T) {
|
||||
event := newEvent()
|
||||
event.Visibility = -1
|
||||
|
||||
err := validator.Validate(&event)
|
||||
assert.EqualError(t, err, "validation error:\n - visibility: value must be one of the defined enum values [enum.defined_only]")
|
||||
})
|
||||
|
||||
t.Run("visibility unspecified", func(t *testing.T) {
|
||||
event := newEvent()
|
||||
event.Visibility = auditV1.Visibility_VISIBILITY_UNSPECIFIED
|
||||
|
||||
err := validator.Validate(&event)
|
||||
assert.EqualError(t, err, "validation error:\n - visibility: value is required [required]")
|
||||
})
|
||||
|
||||
t.Run("object identifier nil", func(t *testing.T) {
|
||||
event := newEvent()
|
||||
event.ObjectIdentifier = nil
|
||||
|
||||
err := validator.Validate(&event)
|
||||
assert.EqualError(t, err, "validation error:\n - object_identifier: value is required [required]")
|
||||
})
|
||||
|
||||
t.Run("object identifier id empty", func(t *testing.T) {
|
||||
event := newEvent()
|
||||
event.ObjectIdentifier.Identifier = ""
|
||||
|
||||
err := validator.Validate(&event)
|
||||
assert.EqualError(t, err, "validation error:\n - object_identifier.identifier: value is required [required]")
|
||||
})
|
||||
|
||||
t.Run("object identifier id not uuid", func(t *testing.T) {
|
||||
event := newEvent()
|
||||
event.ObjectIdentifier.Identifier = "invalid"
|
||||
|
||||
err := validator.Validate(&event)
|
||||
assert.EqualError(t, err, "validation error:\n - object_identifier.identifier: value must be a valid UUID [string.uuid]")
|
||||
})
|
||||
|
||||
t.Run("object identifier type empty", func(t *testing.T) {
|
||||
event := newEvent()
|
||||
event.ObjectIdentifier.Type = ""
|
||||
|
||||
err := validator.Validate(&event)
|
||||
assert.EqualError(t, err, "validation error:\n - object_identifier.type: value is required [required]")
|
||||
})
|
||||
|
||||
t.Run("data nil", func(t *testing.T) {
|
||||
event := newEvent()
|
||||
event.Data = nil
|
||||
|
||||
err := validator.Validate(&event)
|
||||
assert.EqualError(t, err, "validation error:\n - data: exactly one field is required in oneof [required]")
|
||||
})
|
||||
|
||||
t.Run("data empty", func(t *testing.T) {
|
||||
event := newEvent()
|
||||
event.Data = &auditV1.RoutableAuditEvent_UnencryptedData{UnencryptedData: &auditV1.UnencryptedData{
|
||||
Data: []byte{},
|
||||
ProtobufType: "audit.v1.AuditLogEntry",
|
||||
}}
|
||||
|
||||
err := validator.Validate(&event)
|
||||
assert.EqualError(t, err, "validation error:\n - unencrypted_data.data: value is required [required]")
|
||||
})
|
||||
|
||||
t.Run("data protobuf type empty", func(t *testing.T) {
|
||||
event := newEvent()
|
||||
event.Data = &auditV1.RoutableAuditEvent_UnencryptedData{UnencryptedData: &auditV1.UnencryptedData{
|
||||
Data: []byte("data"),
|
||||
ProtobufType: "",
|
||||
}}
|
||||
|
||||
err := validator.Validate(&event)
|
||||
assert.EqualError(t, err, "validation error:\n - unencrypted_data.protobuf_type: value is required [required]")
|
||||
})
|
||||
}
|
||||
462
audit/api/test_data.go
Normal file
462
audit/api/test_data.go
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
const clientCredentialsToken = "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOGJlZjc1LWRmY2QtNGE3My1hMzkxLTU0YTdhZjU3YTdkNiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic3RhY2tpdC1yZXNvdXJjZS1tYW5hZ2VyLWRldiJdLCJjbGllbnRfaWQiOiJzdGFja2l0LXJlc291cmNlLW1hbmFnZXItZGV2IiwiZXhwIjoxNzI0NDA1MzI2LCJpYXQiOjE3MjQ0MDQ0MjYsImlzcyI6Imh0dHBzOi8vYWNjb3VudHMuZGV2LnN0YWNraXQuY2xvdWQiLCJqdGkiOiJlNDZlYmEzOC1kZWRiLTQ1NDEtOTRmMy00OWY5N2E5MzRkNTgiLCJuYmYiOjE3MjQ0MDQ0MjYsInNjb3BlIjoidWFhLm5vbmUiLCJzdWIiOiJzdGFja2l0LXJlc291cmNlLW1hbmFnZXItZGV2In0.JP5Uy7AMdK4ukzQ6aOYzbVwEmq0Tp2ppQGRqGOhuVQgbqs6yJ33GKXo7RPsJVLw3FR7XAxENIVqNvzGotbDXr0NjBGdzyxIHzrOaUqM4w1iLzD1KF51dXFwkoigqDdD7Ze9eI_Uo3tSn8FwGLTSoO-ONQYpnceCiGut2Gc6VIL8HOLdh8dzlRENGQtgYd-3Y5zqpoLrsR2Bd-0sv15sF-5aI0CqcC8gE70JPImKf2u_IYI-TYMDNk86YSCtaYO5-alOrHXXWwgzSoH-r2s5qoOhPbei9myV_P4fdcKXxMqfap9hImXPUooVhpdUr1AabZw3MtW7rION8tJAiauhMQA"
|
||||
const serviceAccountTokenRepeatedlyImpersonated = "Bearer eyJraWQiOiJaVFJqWlRNek5tSmlNRGt3TldJMU5USTRZVGxpT1RjMllUWXlZVE16WldNIiwiYWxnIjoiUlM1MTIifQ.eyJzdWIiOiIxNzM0YjRiNi0xZDVlLTQ4MTktOWI1MC0yOTkxN2ExYjlhZDUiLCJpc3MiOiJzdGFja2l0L3NlcnZpY2VhY2NvdW50IiwiYXVkIjpbInN0YWNraXQiLCJhcGkiXSwic3RhY2tpdC9zZXJ2aWNlYWNjb3VudC90b2tlbi5zb3VyY2UiOiJvYXV0aDIiLCJhY3QiOnsic3ViIjoiZjQ1MDA5YjItNjQzMy00M2MxLWI2YzctNjE4YzQ0MzU5ZTcxIiwiYWN0Ijp7InN1YiI6IjAyYWVmNTE2LTMxN2YtNGVjMS1hMWRmLTFhY2JkNGQ0OWZlMyJ9fSwic3RhY2tpdC9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJhcGkiLCJzdGFja2l0L3Byb2plY3QvcHJvamVjdC5pZCI6ImRhY2M3ODMwLTg0M2UtNGM1ZS04NmZmLWFhMGZiNTFkNjM2ZiIsImF6cCI6ImY0NTAwOWIyLTY0MzMtNDNjMS1iNmM3LTYxOGM0NDM1OWU3MSIsInN0YWNraXQvc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjE3MzRiNGI2LTFkNWUtNDgxOS05YjUwLTI5OTE3YTFiOWFkNSIsImV4cCI6MTcyNDA2Mjk2MywiaWF0IjoxNzI0MDU5MzYzLCJlbWFpbCI6InNlcnZpY2UtYWNjb3VudC0zLWZnaHN4dzFAc2Euc3RhY2tpdC5jbG91ZCIsImp0aSI6IjFmN2YxZWZjLTMzNDktNDExYS1hNWQ3LTIyNTVlMGE1YThhZSJ9.c1ae17bAtyOdmwXQbK37W-NTyOxo7iER5aHS_C0fU1qKl2BjOz708GLjH-_vxx9eKPeYznfI21_xlTaAvuG4Aco9f5YDK7fooTVHnDaOSSggqcEaDzDPrNXhhKEDxotJeq9zRMVCEStcbirjTounnLbuULRbO5GSY5jo-8n2UKxSZ2j5G_SjFHajdJwmzwvOttp08tdL8ck1uDdgVNBfcm0VIdb6WmgrCIUq5rmoa-cRPkdEurNtIEgEB_9U0Xh-SpmmsvFsWWeNIKz0e_5RCIyJonm_wMkGmblGegemkYL76ypeMNXTQsly1RozDIePfzHuZOWbySHSCd-vKQa2kw"
|
||||
const serviceAccountTokenImpersonated = "Bearer eyJraWQiOiJaVFJqWlRNek5tSmlNRGt3TldJMU5USTRZVGxpT1RjMllUWXlZVE16WldNIiwiYWxnIjoiUlM1MTIifQ.eyJzdWIiOiJmNDUwMDliMi02NDMzLTQzYzEtYjZjNy02MThjNDQzNTllNzEiLCJpc3MiOiJzdGFja2l0L3NlcnZpY2VhY2NvdW50IiwiYXVkIjpbInN0YWNraXQiLCJhcGkiXSwic3RhY2tpdC9zZXJ2aWNlYWNjb3VudC90b2tlbi5zb3VyY2UiOiJvYXV0aDIiLCJhY3QiOnsic3ViIjoiMDJhZWY1MTYtMzE3Zi00ZWMxLWExZGYtMWFjYmQ0ZDQ5ZmUzIn0sInN0YWNraXQvc2VydmljZWFjY291bnQvbmFtZXNwYWNlIjoiYXBpIiwic3RhY2tpdC9wcm9qZWN0L3Byb2plY3QuaWQiOiJkYWNjNzgzMC04NDNlLTRjNWUtODZmZi1hYTBmYjUxZDYzNmYiLCJhenAiOiIwMmFlZjUxNi0zMTdmLTRlYzEtYTFkZi0xYWNiZDRkNDlmZTMiLCJzdGFja2l0L3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJmNDUwMDliMi02NDMzLTQzYzEtYjZjNy02MThjNDQzNTllNzEiLCJleHAiOjE3MjQwNjI5MDcsImlhdCI6MTcyNDA1OTMwNywiZW1haWwiOiJzZXJ2aWNlLWFjY291bnQtMi10ajlzcnQxQHNhLnN0YWNraXQuY2xvdWQiLCJqdGkiOiIzNzU1NTE4My0wMWI5LTQyNzAtYmRjMS02OWI0ZmNmZDVlZTkifQ.auBvvsIesFMAlWOCPCPC77DrrHF7gSKZwKs_Zry5KFvu2bpZZC1BcSXOc8b9eh0SzANI9M9aGJBhOzOm39-ZZ5XOQ-6_y1aWuEenYQ6kT5D3GzCUTMDzSi1lcZ4IG5nFMa_AAlVEN_7AMv7LHGtz49bWLJnAgeTo1cvof-OgP4mCQ5O6E0iyAq-5u8V8NJL7HIZy7BDe4J1mjfYhwKagrN7QFWu4fhN4TNS7d922X_6V489BhjRFRYjLW_qDnv912JorbGRz_XwNy_dPA81EkdMyKE0BJUezguJUEKEG2_JEi9O64Flcoi6x8cFHYhaDuMMSLipzePaHdyk2lQtH7Q"
|
||||
const serviceAccountToken = "Bearer eyJraWQiOiJaVFJqWlRNek5tSmlNRGt3TldJMU5USTRZVGxpT1RjMllUWXlZVE16WldNIiwiYWxnIjoiUlM1MTIifQ.eyJzdWIiOiIxMGYzOGIwMS01MzRiLTQ3YmItYTAzYS1lMjk0Y2EyYmU0ZGUiLCJhdWQiOlsic3RhY2tpdCIsImFwaSJdLCJzdGFja2l0L3NlcnZpY2VhY2NvdW50L3Rva2VuLnNvdXJjZSI6ImxlZ2FjeSIsInN0YWNraXQvc2VydmljZWFjY291bnQvbmFtZXNwYWNlIjoiYXBpIiwic3RhY2tpdC9wcm9qZWN0L3Byb2plY3QuaWQiOiJkYWNjNzgzMC04NDNlLTRjNWUtODZmZi1hYTBmYjUxZDYzNmYiLCJhenAiOiJjZDk0ZjAxYS1kZjJlLTQ0NTYtOTAyZS00OGY1ZTU3ZjBiNjMiLCJpc3MiOiJzdGFja2l0L3NlcnZpY2VhY2NvdW50Iiwic3RhY2tpdC9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiMTBmMzhiMDEtNTM0Yi00N2JiLWEwM2EtZTI5NGNhMmJlNGRlIiwiZXhwIjoxNzIyNjY5MzQzLCJpYXQiOjE3MjI1ODI5NDMsImVtYWlsIjoibXktc2VydmljZS15aWZjOWUxQHNhLnN0YWNraXQuY2xvdWQiLCJqdGkiOiI4NGMzMGE0Ni0xMDAxLTQzNmYtODU5Zi04OWMwYmExOWJlMWUifQ.hb8X9VKc9xViHgNMyFHT9ePj_lyEwTV1D2es8E278WtoCJ9-4GPPQGjhcLGGrigjnvpRYV2LKzNqpQslerT5lFT_pHACsryaAE0ImYjmoe-nutA7BBpYuM_JN6pk5VIjVFLTqRKeIvFexPacqS2Vo3YoK1GvxPB8WPWBbGIsBtMl-PTm8OTwwzooBOoCRhhMR-E1lFbAymLsc1JI4yDQKLLomvhEopgmocCnQ-P1QkiKMqdkNxiD_YYLLYTOApg6d62BhqpH66ziqx493AStdZ8d5Kjvf3e1knDhaxVwNCghQj7lSo2kNAqZe__g2tiXpiZNTXBFJ_5HgQMLh67wng"
|
||||
const userToken = "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOGJlZjc1LWRmY2QtNGE3My1hMzkxLTU0YTdhZjU3YTdkNiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic3RhY2tpdC1wb3J0YWwtbG9naW4tZGV2LWNsaWVudC1pZCJdLCJjbGllbnRfaWQiOiJzdGFja2l0LXBvcnRhbC1sb2dpbi1kZXYtY2xpZW50LWlkIiwiZW1haWwiOiJDaHJpc3RpYW4uU2NoYWlibGVAbm92YXRlYy1nbWJoLmRlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6MTcyMjU5MDM2NywiaWF0IjoxNzIyNTg2NzY3LCJpc3MiOiJodHRwczovL2FjY291bnRzLmRldi5zdGFja2l0LmNsb3VkIiwianRpIjoiZDczYTY3YWMtZDFlYy00YjU1LTk5ZDQtZTk1MzI3NWYwMjJhIiwibmJmIjoxNzIyNTg2NzY3LCJzY29wZSI6Im9wZW5pZCBlbWFpbCIsInN1YiI6ImNkOTRmMDFhLWRmMmUtNDQ1Ni05MDJlLTQ4ZjVlNTdmMGI2MyJ9.ajhjYbC5l5g7un9NSheoAwBT83YcZM91rH4DJxPTDsB78HzIVrmaKTPrK3AI_E1THlD2Z3_ot9nFr_eX7XcwWp_ZBlataKmakdXlAmeb4xSMGNYefIfzV_3w9ZZAZ66yoeTrtn8dUx5ezquenCYpctB1NcccmK4U09V0kNcq9dFcfF3Sg9YilF3orUCR0ql1d9RnOs3EiFZuUpdBEkyoVsAdSh2P-PRbNViR_FgCcAJem97TsN5CQc9RlvKYe4sYKgqQoqa2GDVi9Niiw3fe1V8SCnROYcpkOzBBWdvuzFMBUjln3uOogYVOz93xkmImV6jidgyQ70fLt-eDUmZZfg"
|
||||
|
||||
var TestHeaders = map[string][]string{"user-agent": {"custom"}, "authorization": {userToken}}
|
||||
|
||||
func newOrganizationAuditEvent(
|
||||
customization *func(
|
||||
*auditV1.AuditLogEntry,
|
||||
*auditV1.ObjectIdentifier,
|
||||
)) (
|
||||
*auditV1.AuditLogEntry,
|
||||
*auditV1.ObjectIdentifier,
|
||||
) {
|
||||
|
||||
identifier := uuid.New()
|
||||
permission := "resourcemanager.organization.edit"
|
||||
permissionGranted := true
|
||||
requestId := fmt.Sprintf("%s/1", identifier)
|
||||
claims, _ := structpb.NewStruct(map[string]interface{}{})
|
||||
correlationId := "cad100e2-e139-43b9-8c3b-335731e032bc"
|
||||
headers := make(map[string]string)
|
||||
headers["Content-Type"] = "application/json"
|
||||
labels := make(map[string]string)
|
||||
labels["label1"] = "value1"
|
||||
auditEvent := &auditV1.AuditLogEntry{
|
||||
LogName: fmt.Sprintf("%s/%s/logs/%s", ObjectTypeOrganization.Plural(), identifier, EventTypeAdminActivity),
|
||||
ProtoPayload: &auditV1.AuditLog{
|
||||
ServiceName: "resource-manager",
|
||||
OperationName: "stackit.resourcemanager.v2.organization.created",
|
||||
ResourceName: fmt.Sprintf("%s/%s", ObjectTypeOrganization.Plural(), identifier),
|
||||
AuthenticationInfo: &auditV1.AuthenticationInfo{
|
||||
PrincipalId: uuid.NewString(),
|
||||
PrincipalEmail: "user@example.com",
|
||||
ServiceAccountName: nil,
|
||||
ServiceAccountDelegationInfo: nil,
|
||||
},
|
||||
AuthorizationInfo: []*auditV1.AuthorizationInfo{{
|
||||
Resource: fmt.Sprintf("%s/%s", ObjectTypeOrganization.Plural(), identifier),
|
||||
Permission: &permission,
|
||||
Granted: &permissionGranted,
|
||||
}},
|
||||
RequestMetadata: &auditV1.RequestMetadata{
|
||||
CallerIp: "127.0.0.1",
|
||||
CallerSuppliedUserAgent: "OpenAPI-Generator/ 1.0.0/ go",
|
||||
RequestAttributes: &auditV1.AttributeContext_Request{
|
||||
Id: &requestId,
|
||||
Method: auditV1.AttributeContext_HTTP_METHOD_POST,
|
||||
Headers: headers,
|
||||
Path: "/v2/organizations",
|
||||
Host: "stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud",
|
||||
Scheme: "https",
|
||||
Query: nil,
|
||||
Time: timestamppb.New(time.Now().UTC()),
|
||||
Protocol: "http/1.1",
|
||||
Auth: &auditV1.AttributeContext_Auth{
|
||||
Principal: "https%3A%2F%2Faccounts.dev.stackit.cloud/stackit-resource-manager-dev",
|
||||
Audiences: []string{"https:// stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud", "stackit", "api"},
|
||||
Claims: claims,
|
||||
},
|
||||
},
|
||||
},
|
||||
Request: nil,
|
||||
ResponseMetadata: &auditV1.ResponseMetadata{
|
||||
StatusCode: wrapperspb.Int32(200),
|
||||
ErrorMessage: nil,
|
||||
ErrorDetails: nil,
|
||||
ResponseAttributes: &auditV1.AttributeContext_Response{
|
||||
NumResponseItems: nil,
|
||||
Size: nil,
|
||||
Headers: nil,
|
||||
Time: timestamppb.New(time.Now().UTC()),
|
||||
},
|
||||
},
|
||||
Response: nil,
|
||||
Metadata: nil,
|
||||
},
|
||||
InsertId: fmt.Sprintf("%d/eu01/e72182e8-0bb9-4be2-a19f-87fc0dd6e738/00000000001", time.Now().UnixNano()),
|
||||
Labels: labels,
|
||||
CorrelationId: &correlationId,
|
||||
Timestamp: timestamppb.New(time.Now()),
|
||||
Severity: auditV1.LogSeverity_LOG_SEVERITY_DEFAULT,
|
||||
TraceParent: nil,
|
||||
TraceState: nil,
|
||||
}
|
||||
|
||||
objectIdentifier := &auditV1.ObjectIdentifier{
|
||||
Identifier: identifier.String(),
|
||||
Type: string(ObjectTypeOrganization),
|
||||
}
|
||||
|
||||
if customization != nil {
|
||||
(*customization)(auditEvent, objectIdentifier)
|
||||
}
|
||||
|
||||
return auditEvent, objectIdentifier
|
||||
}
|
||||
|
||||
func newFolderAuditEvent(
|
||||
customization *func(
|
||||
*auditV1.AuditLogEntry,
|
||||
*auditV1.ObjectIdentifier,
|
||||
)) (
|
||||
*auditV1.AuditLogEntry,
|
||||
*auditV1.ObjectIdentifier,
|
||||
) {
|
||||
|
||||
identifier := uuid.New()
|
||||
permission := "resourcemanager.folder.edit"
|
||||
permissionGranted := true
|
||||
requestId := fmt.Sprintf("%s/1", identifier)
|
||||
claims, _ := structpb.NewStruct(map[string]interface{}{})
|
||||
correlationId := "9c71cedf-ca52-4f9c-a519-ed006e810cdd"
|
||||
headers := make(map[string]string)
|
||||
headers["Content-Type"] = "application/json"
|
||||
labels := make(map[string]string)
|
||||
labels["label1"] = "value1"
|
||||
auditEvent := &auditV1.AuditLogEntry{
|
||||
LogName: fmt.Sprintf("%s/%s/logs/%s", ObjectTypeFolder.Plural(), identifier, EventTypeAdminActivity),
|
||||
ProtoPayload: &auditV1.AuditLog{
|
||||
ServiceName: "resource-manager",
|
||||
OperationName: "stackit.resourcemanager.v2.folder.created",
|
||||
ResourceName: fmt.Sprintf("%s/%s", ObjectTypeFolder.Plural(), identifier),
|
||||
AuthenticationInfo: &auditV1.AuthenticationInfo{
|
||||
PrincipalId: uuid.NewString(),
|
||||
PrincipalEmail: "user@example.com",
|
||||
ServiceAccountName: nil,
|
||||
ServiceAccountDelegationInfo: nil,
|
||||
},
|
||||
AuthorizationInfo: []*auditV1.AuthorizationInfo{{
|
||||
Resource: fmt.Sprintf("%s/%s", ObjectTypeFolder.Plural(), identifier),
|
||||
Permission: &permission,
|
||||
Granted: &permissionGranted,
|
||||
}},
|
||||
RequestMetadata: &auditV1.RequestMetadata{
|
||||
CallerIp: "127.0.0.1",
|
||||
CallerSuppliedUserAgent: "OpenAPI-Generator/ 1.0.0/ go",
|
||||
RequestAttributes: &auditV1.AttributeContext_Request{
|
||||
Id: &requestId,
|
||||
Method: auditV1.AttributeContext_HTTP_METHOD_POST,
|
||||
Headers: headers,
|
||||
Path: "/v2/folders",
|
||||
Host: "stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud",
|
||||
Scheme: "https",
|
||||
Query: nil,
|
||||
Time: timestamppb.New(time.Now().UTC()),
|
||||
Protocol: "http/1.1",
|
||||
Auth: &auditV1.AttributeContext_Auth{
|
||||
Principal: "https%3A%2F%2Faccounts.dev.stackit.cloud/stackit-resource-manager-dev",
|
||||
Audiences: []string{"https:// stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud", "stackit", "api"},
|
||||
Claims: claims,
|
||||
},
|
||||
},
|
||||
},
|
||||
Request: nil,
|
||||
ResponseMetadata: &auditV1.ResponseMetadata{
|
||||
StatusCode: wrapperspb.Int32(200),
|
||||
ErrorMessage: nil,
|
||||
ErrorDetails: nil,
|
||||
ResponseAttributes: &auditV1.AttributeContext_Response{
|
||||
NumResponseItems: nil,
|
||||
Size: nil,
|
||||
Headers: nil,
|
||||
Time: timestamppb.New(time.Now().UTC()),
|
||||
},
|
||||
},
|
||||
Response: nil,
|
||||
Metadata: nil,
|
||||
},
|
||||
InsertId: fmt.Sprintf("%d/eu01/e72182e8-0bb9-4be2-a19f-87fc0dd6e738/00000000001", time.Now().UnixNano()),
|
||||
Labels: labels,
|
||||
CorrelationId: &correlationId,
|
||||
Timestamp: timestamppb.New(time.Now()),
|
||||
Severity: auditV1.LogSeverity_LOG_SEVERITY_DEFAULT,
|
||||
TraceParent: nil,
|
||||
TraceState: nil,
|
||||
}
|
||||
|
||||
objectIdentifier := &auditV1.ObjectIdentifier{
|
||||
Identifier: identifier.String(),
|
||||
Type: string(ObjectTypeFolder),
|
||||
}
|
||||
|
||||
if customization != nil {
|
||||
(*customization)(auditEvent, objectIdentifier)
|
||||
}
|
||||
|
||||
return auditEvent, objectIdentifier
|
||||
}
|
||||
|
||||
func newProjectAuditEvent(
|
||||
customization *func(
|
||||
*auditV1.AuditLogEntry,
|
||||
*auditV1.ObjectIdentifier,
|
||||
)) (
|
||||
*auditV1.AuditLogEntry,
|
||||
*auditV1.ObjectIdentifier,
|
||||
) {
|
||||
|
||||
identifier := uuid.New()
|
||||
permission := "resourcemanager.project.edit"
|
||||
permissionGranted := true
|
||||
requestId := fmt.Sprintf("%s/1", identifier)
|
||||
claims, _ := structpb.NewStruct(map[string]interface{}{})
|
||||
correlationId := "14d5b611-ccce-4cfa-9085-9ccbfccce3cb"
|
||||
headers := make(map[string]string)
|
||||
headers["Content-Type"] = "application/json"
|
||||
labels := make(map[string]string)
|
||||
labels["label1"] = "value1"
|
||||
auditEvent := &auditV1.AuditLogEntry{
|
||||
LogName: fmt.Sprintf("%s/%s/logs/%s", ObjectTypeProject.Plural(), identifier, EventTypeAdminActivity),
|
||||
ProtoPayload: &auditV1.AuditLog{
|
||||
ServiceName: "resource-manager",
|
||||
OperationName: "stackit.resourcemanager.v2.project.created",
|
||||
ResourceName: fmt.Sprintf("%s/%s", ObjectTypeProject.Plural(), identifier),
|
||||
AuthenticationInfo: &auditV1.AuthenticationInfo{
|
||||
PrincipalId: uuid.NewString(),
|
||||
PrincipalEmail: "user@example.com",
|
||||
ServiceAccountName: nil,
|
||||
ServiceAccountDelegationInfo: nil,
|
||||
},
|
||||
AuthorizationInfo: []*auditV1.AuthorizationInfo{{
|
||||
Resource: fmt.Sprintf("%s/%s", ObjectTypeProject.Plural(), identifier),
|
||||
Permission: &permission,
|
||||
Granted: &permissionGranted,
|
||||
}},
|
||||
RequestMetadata: &auditV1.RequestMetadata{
|
||||
CallerIp: "127.0.0.1",
|
||||
CallerSuppliedUserAgent: "OpenAPI-Generator/ 1.0.0/ go",
|
||||
RequestAttributes: &auditV1.AttributeContext_Request{
|
||||
Id: &requestId,
|
||||
Method: auditV1.AttributeContext_HTTP_METHOD_POST,
|
||||
Headers: headers,
|
||||
Path: "/v2/projects",
|
||||
Host: "stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud",
|
||||
Scheme: "https",
|
||||
Query: nil,
|
||||
Time: timestamppb.New(time.Now().UTC()),
|
||||
Protocol: "http/1.1",
|
||||
Auth: &auditV1.AttributeContext_Auth{
|
||||
Principal: "https%3A%2F%2Faccounts.dev.stackit.cloud/stackit-resource-manager-dev",
|
||||
Audiences: []string{"https:// stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud", "stackit", "api"},
|
||||
Claims: claims,
|
||||
},
|
||||
},
|
||||
},
|
||||
Request: nil,
|
||||
ResponseMetadata: &auditV1.ResponseMetadata{
|
||||
StatusCode: wrapperspb.Int32(200),
|
||||
ErrorMessage: nil,
|
||||
ErrorDetails: nil,
|
||||
ResponseAttributes: &auditV1.AttributeContext_Response{
|
||||
NumResponseItems: nil,
|
||||
Size: nil,
|
||||
Headers: nil,
|
||||
Time: timestamppb.New(time.Now().UTC()),
|
||||
},
|
||||
},
|
||||
Response: nil,
|
||||
Metadata: nil,
|
||||
},
|
||||
InsertId: fmt.Sprintf("%d/eu01/e72182e8-0bb9-4be2-a19f-87fc0dd6e738/00000000001", time.Now().UnixNano()),
|
||||
Labels: labels,
|
||||
CorrelationId: &correlationId,
|
||||
Timestamp: timestamppb.New(time.Now()),
|
||||
Severity: auditV1.LogSeverity_LOG_SEVERITY_DEFAULT,
|
||||
TraceParent: nil,
|
||||
TraceState: nil,
|
||||
}
|
||||
|
||||
objectIdentifier := &auditV1.ObjectIdentifier{
|
||||
Identifier: identifier.String(),
|
||||
Type: string(ObjectTypeProject),
|
||||
}
|
||||
|
||||
if customization != nil {
|
||||
(*customization)(auditEvent, objectIdentifier)
|
||||
}
|
||||
|
||||
return auditEvent, objectIdentifier
|
||||
}
|
||||
|
||||
func newProjectSystemAuditEvent(
|
||||
customization *func(*auditV1.AuditLogEntry)) *auditV1.AuditLogEntry {
|
||||
|
||||
identifier := uuid.New()
|
||||
requestId := fmt.Sprintf("%s/1", identifier)
|
||||
claims, _ := structpb.NewStruct(map[string]interface{}{})
|
||||
correlationId := "9b5a8e9b-32a0-435f-b97b-a9a42b9e016b"
|
||||
headers := make(map[string]string)
|
||||
headers["Content-Type"] = "application/json"
|
||||
labels := make(map[string]string)
|
||||
labels["label1"] = "value1"
|
||||
serviceAccountId := uuid.NewString()
|
||||
serviceAccountName := fmt.Sprintf("projects/%s/service-accounts/%s", identifier, serviceAccountId)
|
||||
delegationPrincipal := auditV1.ServiceAccountDelegationInfo{Authority: &auditV1.ServiceAccountDelegationInfo_SystemPrincipal_{}}
|
||||
auditEvent := &auditV1.AuditLogEntry{
|
||||
LogName: fmt.Sprintf("%s/%s/logs/%s", SystemIdentifier.Type, SystemIdentifier.Identifier, EventTypeSystemEvent),
|
||||
ProtoPayload: &auditV1.AuditLog{
|
||||
ServiceName: "resource-manager",
|
||||
OperationName: "stackit.resourcemanager.v2.system.changed",
|
||||
ResourceName: fmt.Sprintf("%s/%s", ObjectTypeProject.Plural(), identifier),
|
||||
AuthenticationInfo: &auditV1.AuthenticationInfo{
|
||||
PrincipalId: serviceAccountId,
|
||||
PrincipalEmail: "service-account@sa.stackit.cloud",
|
||||
ServiceAccountName: &serviceAccountName,
|
||||
ServiceAccountDelegationInfo: []*auditV1.ServiceAccountDelegationInfo{&delegationPrincipal},
|
||||
},
|
||||
AuthorizationInfo: []*auditV1.AuthorizationInfo{{
|
||||
Resource: fmt.Sprintf("%s/%s", ObjectTypeProject.Plural(), identifier),
|
||||
Permission: nil,
|
||||
Granted: nil,
|
||||
}},
|
||||
RequestMetadata: &auditV1.RequestMetadata{
|
||||
CallerIp: "127.0.0.1",
|
||||
CallerSuppliedUserAgent: "OpenAPI-Generator/ 1.0.0/ go",
|
||||
RequestAttributes: &auditV1.AttributeContext_Request{
|
||||
Id: &requestId,
|
||||
Method: auditV1.AttributeContext_HTTP_METHOD_POST,
|
||||
Headers: headers,
|
||||
Path: "/v2/projects",
|
||||
Host: "stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud",
|
||||
Scheme: "https",
|
||||
Query: nil,
|
||||
Time: timestamppb.New(time.Now().UTC()),
|
||||
Protocol: "http/1.1",
|
||||
Auth: &auditV1.AttributeContext_Auth{
|
||||
Principal: "https%3A%2F%2Faccounts.dev.stackit.cloud/stackit-resource-manager-dev",
|
||||
Audiences: []string{"https:// stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud", "stackit", "api"},
|
||||
Claims: claims,
|
||||
},
|
||||
},
|
||||
},
|
||||
Request: nil,
|
||||
ResponseMetadata: &auditV1.ResponseMetadata{
|
||||
StatusCode: wrapperspb.Int32(200),
|
||||
ErrorMessage: nil,
|
||||
ErrorDetails: nil,
|
||||
ResponseAttributes: &auditV1.AttributeContext_Response{
|
||||
NumResponseItems: nil,
|
||||
Size: nil,
|
||||
Headers: nil,
|
||||
Time: timestamppb.New(time.Now().UTC()),
|
||||
},
|
||||
},
|
||||
Response: nil,
|
||||
Metadata: nil,
|
||||
},
|
||||
InsertId: fmt.Sprintf("%d/eu01/e72182e8-0bb9-4be2-a19f-87fc0dd6e738/00000000001", time.Now().UnixNano()),
|
||||
Labels: labels,
|
||||
CorrelationId: &correlationId,
|
||||
Timestamp: timestamppb.New(time.Now()),
|
||||
Severity: auditV1.LogSeverity_LOG_SEVERITY_DEFAULT,
|
||||
TraceParent: nil,
|
||||
TraceState: nil,
|
||||
}
|
||||
|
||||
if customization != nil {
|
||||
(*customization)(auditEvent)
|
||||
}
|
||||
|
||||
return auditEvent
|
||||
}
|
||||
|
||||
func newSystemAuditEvent(
|
||||
customization *func(*auditV1.AuditLogEntry)) *auditV1.AuditLogEntry {
|
||||
|
||||
identifier := uuid.Nil
|
||||
requestId := fmt.Sprintf("%s/1", identifier)
|
||||
claims, _ := structpb.NewStruct(map[string]interface{}{})
|
||||
correlationId := "14d5b611-ccce-4cfa-9085-9ccbfccce3cb"
|
||||
headers := make(map[string]string)
|
||||
headers["Content-Type"] = "application/json"
|
||||
labels := make(map[string]string)
|
||||
labels["label1"] = "value1"
|
||||
serviceAccountId := uuid.NewString()
|
||||
serviceAccountName := fmt.Sprintf("projects/%s/service-accounts/%s", identifier, serviceAccountId)
|
||||
delegationPrincipal := auditV1.ServiceAccountDelegationInfo{Authority: &auditV1.ServiceAccountDelegationInfo_SystemPrincipal_{}}
|
||||
auditEvent := &auditV1.AuditLogEntry{
|
||||
LogName: fmt.Sprintf("%s/%s/logs/%s", ObjectTypeSystem.Plural(), identifier, EventTypeSystemEvent),
|
||||
ProtoPayload: &auditV1.AuditLog{
|
||||
ServiceName: "resource-manager",
|
||||
OperationName: "stackit.resourcemanager.v2.system.changed",
|
||||
ResourceName: fmt.Sprintf("%s/%s", ObjectTypeSystem.Plural(), identifier),
|
||||
AuthenticationInfo: &auditV1.AuthenticationInfo{
|
||||
PrincipalId: serviceAccountId,
|
||||
PrincipalEmail: "service-account@sa.stackit.cloud",
|
||||
ServiceAccountName: &serviceAccountName,
|
||||
ServiceAccountDelegationInfo: []*auditV1.ServiceAccountDelegationInfo{&delegationPrincipal},
|
||||
},
|
||||
AuthorizationInfo: []*auditV1.AuthorizationInfo{{
|
||||
Resource: fmt.Sprintf("%s/%s", ObjectTypeSystem.Plural(), identifier),
|
||||
Permission: nil,
|
||||
Granted: nil,
|
||||
}},
|
||||
RequestMetadata: &auditV1.RequestMetadata{
|
||||
CallerIp: "127.0.0.1",
|
||||
CallerSuppliedUserAgent: "OpenAPI-Generator/ 1.0.0/ go",
|
||||
RequestAttributes: &auditV1.AttributeContext_Request{
|
||||
Id: &requestId,
|
||||
Method: auditV1.AttributeContext_HTTP_METHOD_POST,
|
||||
Headers: headers,
|
||||
Path: "/v2/projects",
|
||||
Host: "stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud",
|
||||
Scheme: "https",
|
||||
Query: nil,
|
||||
Time: timestamppb.New(time.Now().UTC()),
|
||||
Protocol: "http/1.1",
|
||||
Auth: &auditV1.AttributeContext_Auth{
|
||||
Principal: "https%3A%2F%2Faccounts.dev.stackit.cloud/stackit-resource-manager-dev",
|
||||
Audiences: []string{"https:// stackit-resource-manager-dev.apps.01.cf.eu01.stackit.cloud", "stackit", "api"},
|
||||
Claims: claims,
|
||||
},
|
||||
},
|
||||
},
|
||||
Request: nil,
|
||||
ResponseMetadata: &auditV1.ResponseMetadata{
|
||||
StatusCode: wrapperspb.Int32(200),
|
||||
ErrorMessage: nil,
|
||||
ErrorDetails: nil,
|
||||
ResponseAttributes: &auditV1.AttributeContext_Response{
|
||||
NumResponseItems: nil,
|
||||
Size: nil,
|
||||
Headers: nil,
|
||||
Time: timestamppb.New(time.Now().UTC()),
|
||||
},
|
||||
},
|
||||
Response: nil,
|
||||
Metadata: nil,
|
||||
},
|
||||
InsertId: fmt.Sprintf("%d/eu01/e72182e8-0bb9-4be2-a19f-87fc0dd6e738/00000000001", time.Now().UnixNano()),
|
||||
Labels: labels,
|
||||
CorrelationId: &correlationId,
|
||||
Timestamp: timestamppb.New(time.Now()),
|
||||
Severity: auditV1.LogSeverity_LOG_SEVERITY_DEFAULT,
|
||||
TraceParent: nil,
|
||||
TraceState: nil,
|
||||
}
|
||||
|
||||
if customization != nil {
|
||||
(*customization)(auditEvent)
|
||||
}
|
||||
|
||||
return auditEvent
|
||||
}
|
||||
218
audit/messaging/messaging.go
Normal file
218
audit/messaging/messaging.go
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
package messaging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/log"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Azure/go-amqp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Default connection timeout for the AMQP connection
|
||||
const connectionTimeoutSeconds = 10
|
||||
|
||||
// Api is an abstraction for a messaging system that can be used to send
|
||||
// audit logs to the audit log system.
|
||||
type Api interface {
|
||||
|
||||
// Send method will send the given data to the specified topic synchronously.
|
||||
// Parameters:
|
||||
// * ctx - the context object
|
||||
// * topic - the messaging topic where to send the data to
|
||||
// * data - the serialized data as byte array
|
||||
// * contentType - the contentType of the serialized data
|
||||
// * applicationProperties - properties to send with the message (i.e. cloud event headers)
|
||||
//
|
||||
// It returns technical errors for connection issues or sending problems.
|
||||
Send(ctx context.Context, topic string, data []byte, contentType string, applicationProperties map[string]any) error
|
||||
}
|
||||
|
||||
// MutexApi is wrapper around an API implementation that controls mutual exclusive access to the api.
|
||||
type MutexApi struct {
|
||||
mutex *sync.Mutex
|
||||
api *Api
|
||||
}
|
||||
|
||||
func NewMutexApi(api *Api) (*Api, error) {
|
||||
if api == nil {
|
||||
return nil, errors.New("api is nil")
|
||||
}
|
||||
mutexApi := MutexApi{
|
||||
mutex: &sync.Mutex{},
|
||||
api: api,
|
||||
}
|
||||
|
||||
var genericApi Api = &mutexApi
|
||||
return &genericApi, nil
|
||||
}
|
||||
|
||||
// Send implements Api.Send
|
||||
func (m *MutexApi) Send(ctx context.Context, topic string, data []byte, contentType string, applicationProperties map[string]any) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
return (*m.api).Send(ctx, topic, data, contentType, applicationProperties)
|
||||
}
|
||||
|
||||
// AmqpConfig provides AMQP connection related parameters.
|
||||
type AmqpConfig struct {
|
||||
URL string
|
||||
User string
|
||||
Password string
|
||||
}
|
||||
|
||||
// AmqpSession is an abstraction providing a subset of the methods of amqp.Session
|
||||
type AmqpSession interface {
|
||||
NewSender(ctx context.Context, target string, opts *amqp.SenderOptions) (*AmqpSender, error)
|
||||
Close(ctx context.Context) error
|
||||
}
|
||||
|
||||
type AmqpSessionWrapper struct {
|
||||
session *amqp.Session
|
||||
}
|
||||
|
||||
func (w AmqpSessionWrapper) NewSender(ctx context.Context, target string, opts *amqp.SenderOptions) (*AmqpSender, error) {
|
||||
sender, err := w.session.NewSender(ctx, target, opts)
|
||||
var amqpSender AmqpSender = sender
|
||||
return &amqpSender, err
|
||||
}
|
||||
|
||||
func (w AmqpSessionWrapper) Close(ctx context.Context) error {
|
||||
return w.session.Close(ctx)
|
||||
}
|
||||
|
||||
// AmqpSender is an abstraction providing a subset of the methods of amqp.Sender
|
||||
type AmqpSender interface {
|
||||
Send(ctx context.Context, msg *amqp.Message, opts *amqp.SendOptions) error
|
||||
Close(ctx context.Context) error
|
||||
}
|
||||
|
||||
// AmqpApi implements Api.
|
||||
type AmqpApi struct {
|
||||
config AmqpConfig
|
||||
connection *amqp.Conn
|
||||
session *AmqpSession
|
||||
}
|
||||
|
||||
func NewAmqpApi(amqpConfig AmqpConfig) (*Api, error) {
|
||||
amqpApi := &AmqpApi{config: amqpConfig}
|
||||
|
||||
err := amqpApi.connect()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var messagingApi Api = amqpApi
|
||||
return &messagingApi, nil
|
||||
}
|
||||
|
||||
// connect opens a new connection and session to the AMQP messaging system.
|
||||
// The connection attempt will be cancelled after connectionTimeoutSeconds.
|
||||
func (a *AmqpApi) connect() error {
|
||||
log.AuditLogger.Info("connecting to messaging system")
|
||||
|
||||
// Set credentials if specified
|
||||
auth := amqp.SASLTypeAnonymous()
|
||||
|
||||
if a.config.User != "" && a.config.Password != "" {
|
||||
auth = amqp.SASLTypePlain(a.config.User, a.config.Password)
|
||||
log.AuditLogger.Info("using username and password for messaging")
|
||||
} else {
|
||||
log.AuditLogger.Warn("using anonymous messaging!")
|
||||
}
|
||||
|
||||
options := &amqp.ConnOptions{
|
||||
SASLType: auth,
|
||||
}
|
||||
|
||||
// Create new context with timeout for the connection initialization
|
||||
subCtx, cancel := context.WithTimeout(context.Background(), connectionTimeoutSeconds*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Initialize connection
|
||||
conn, err := amqp.Dial(subCtx, a.config.URL, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.connection = conn
|
||||
|
||||
// Initialize session
|
||||
session, err := conn.NewSession(context.Background(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var amqpSession AmqpSession = AmqpSessionWrapper{session: session}
|
||||
a.session = &amqpSession
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send implements Api.Send.
|
||||
// If errors occur the connection to the messaging system will be closed and re-established.
|
||||
func (a *AmqpApi) Send(ctx context.Context, topic string, data []byte, contentType string, applicationProperties map[string]any) error {
|
||||
err := a.trySend(ctx, topic, data, contentType, applicationProperties)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Drop the current sender, as it cannot connect to the broker anymore
|
||||
log.AuditLogger.Error("message sender error, recreating", err)
|
||||
|
||||
err = a.resetConnection(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.trySend(ctx, topic, data, contentType, applicationProperties)
|
||||
}
|
||||
|
||||
// trySend actually sends the given data as amqp.Message to the messaging system.
|
||||
func (a *AmqpApi) trySend(ctx context.Context, topic string, data []byte, contentType string, applicationProperties map[string]any) error {
|
||||
if !strings.HasPrefix(topic, AmqpTopicPrefix) {
|
||||
return fmt.Errorf(
|
||||
"topic %q name lacks mandatory prefix %q",
|
||||
topic,
|
||||
AmqpTopicPrefix,
|
||||
)
|
||||
}
|
||||
|
||||
sender, err := (*a.session).NewSender(ctx, topic, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bytes := [][]byte{data}
|
||||
message := amqp.Message{
|
||||
Header: &amqp.MessageHeader{
|
||||
Durable: true,
|
||||
},
|
||||
Properties: &amqp.MessageProperties{
|
||||
To: &topic,
|
||||
ContentType: &contentType,
|
||||
},
|
||||
ApplicationProperties: applicationProperties,
|
||||
Data: bytes,
|
||||
}
|
||||
|
||||
err = (*sender).Send(ctx, &message, nil)
|
||||
if err != nil {
|
||||
_ = (*sender).Close(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resetConnection closes the current session and connection and reconnects to the messaging system.
|
||||
func (a *AmqpApi) resetConnection(ctx context.Context) error {
|
||||
_ = (*a.session).Close(ctx)
|
||||
err := a.connection.Close()
|
||||
if err != nil {
|
||||
log.AuditLogger.Error("failed to close message connection", err)
|
||||
}
|
||||
|
||||
return a.connect()
|
||||
}
|
||||
163
audit/messaging/messaging_test.go
Normal file
163
audit/messaging/messaging_test.go
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
package messaging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Azure/go-amqp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AmqpSessionMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *AmqpSessionMock) NewSender(ctx context.Context, target string, opts *amqp.SenderOptions) (*AmqpSender, error) {
|
||||
args := m.Called(ctx, target, opts)
|
||||
var sender *AmqpSender = nil
|
||||
if args.Get(0) != nil {
|
||||
sender = args.Get(0).(*AmqpSender)
|
||||
}
|
||||
err := args.Error(1)
|
||||
return sender, err
|
||||
}
|
||||
|
||||
func (m *AmqpSessionMock) Close(ctx context.Context) error {
|
||||
args := m.Called(ctx)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
type AmqpSenderMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *AmqpSenderMock) Send(ctx context.Context, msg *amqp.Message, opts *amqp.SendOptions) error {
|
||||
args := m.Called(ctx, msg, opts)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *AmqpSenderMock) Close(ctx context.Context) error {
|
||||
args := m.Called(ctx)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func Test_NewAmqpMessagingApi(t *testing.T) {
|
||||
_, err := NewAmqpApi(AmqpConfig{URL: "not-handled-protocol://localhost:5672"})
|
||||
assert.EqualError(t, err, "unsupported scheme \"not-handled-protocol\"")
|
||||
}
|
||||
|
||||
func Test_AmqpMessagingApi_Send(t *testing.T) {
|
||||
// Specify test timeout
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancelFn()
|
||||
|
||||
// Start solace docker container
|
||||
solaceContainer, err := NewSolaceContainer(context.Background())
|
||||
assert.NoError(t, err)
|
||||
defer solaceContainer.Stop()
|
||||
|
||||
t.Run("Missing topic prefix", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
api, err := NewAmqpApi(AmqpConfig{URL: solaceContainer.AmqpConnectionString})
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = (*api).Send(ctx, "topic-name", []byte{}, "application/json", make(map[string]any))
|
||||
assert.EqualError(t, err, "topic \"topic-name\" name lacks mandatory prefix \"topic://\"")
|
||||
})
|
||||
|
||||
t.Run("New sender call returns error", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Initialize the solace queue
|
||||
topicSubscriptionTopicPattern := "auditlog/>"
|
||||
queueName := "messaging-new-sender"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
topicName := fmt.Sprintf("topic://auditlog/%s", "amqp-no-new-sender")
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
api := &AmqpApi{config: AmqpConfig{URL: solaceContainer.AmqpConnectionString}}
|
||||
err := api.connect()
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedError := errors.New("expected error")
|
||||
|
||||
// Set mock session
|
||||
sessionMock := AmqpSessionMock{}
|
||||
sessionMock.On("NewSender", mock.Anything, mock.Anything, mock.Anything).Return(nil, expectedError)
|
||||
sessionMock.On("Close", mock.Anything).Return(nil)
|
||||
|
||||
var amqpSession AmqpSession = &sessionMock
|
||||
api.session = &amqpSession
|
||||
|
||||
// It's expected that the test succeeds.
|
||||
// First the session is closed as it returns the expected error
|
||||
// Then the retry mechanism restarts the connection and successfully sends the data
|
||||
value := "test"
|
||||
err = (*api).Send(ctx, topicName, []byte(value), "application/json", make(map[string]any))
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check that the mock was called
|
||||
assert.True(t, sessionMock.AssertNumberOfCalls(t, "NewSender", 1))
|
||||
assert.True(t, sessionMock.AssertNumberOfCalls(t, "Close", 1))
|
||||
|
||||
message, err := solaceContainer.NextMessage(ctx, fmt.Sprintf("queue://%s", queueName), true)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, value, string(message.Data[0]))
|
||||
assert.Equal(t, topicName, *message.Properties.To)
|
||||
})
|
||||
|
||||
t.Run("Send call on sender returns error", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Initialize the solace queue
|
||||
topicSubscriptionTopicPattern := "auditlog/>"
|
||||
queueName := "messaging-sender-error"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
topicName := fmt.Sprintf("topic://auditlog/%s", "amqp-sender-error")
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
api := &AmqpApi{config: AmqpConfig{URL: solaceContainer.AmqpConnectionString}}
|
||||
err := api.connect()
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedError := errors.New("expected error")
|
||||
|
||||
// Instantiate mock sender
|
||||
senderMock := AmqpSenderMock{}
|
||||
senderMock.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(expectedError)
|
||||
senderMock.On("Close", mock.Anything).Return(nil)
|
||||
var amqpSender AmqpSender = &senderMock
|
||||
|
||||
// Set mock session
|
||||
sessionMock := AmqpSessionMock{}
|
||||
sessionMock.On("NewSender", mock.Anything, mock.Anything, mock.Anything).Return(&amqpSender, nil)
|
||||
sessionMock.On("Close", mock.Anything).Return(nil)
|
||||
|
||||
var amqpSession AmqpSession = &sessionMock
|
||||
api.session = &amqpSession
|
||||
|
||||
// It's expected that the test succeeds.
|
||||
// First the sender and session are closed as the sender returns the expected error
|
||||
// Then the retry mechanism restarts the connection and successfully sends the data
|
||||
value := "test"
|
||||
err = (*api).Send(ctx, topicName, []byte(value), "application/json", make(map[string]any))
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check that the mocks were called
|
||||
assert.True(t, sessionMock.AssertNumberOfCalls(t, "NewSender", 1))
|
||||
assert.True(t, sessionMock.AssertNumberOfCalls(t, "Close", 1))
|
||||
assert.True(t, senderMock.AssertNumberOfCalls(t, "Send", 1))
|
||||
assert.True(t, senderMock.AssertNumberOfCalls(t, "Close", 1))
|
||||
|
||||
message, err := solaceContainer.NextMessage(ctx, fmt.Sprintf("queue://%s", queueName), true)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, value, string(message.Data[0]))
|
||||
assert.Equal(t, topicName, *message.Properties.To)
|
||||
})
|
||||
}
|
||||
438
audit/messaging/solace.go
Normal file
438
audit/messaging/solace.go
Normal file
|
|
@ -0,0 +1,438 @@
|
|||
package messaging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/log"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Azure/go-amqp"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
AmqpTopicPrefix = "topic://"
|
||||
AmqpQueuePrefix = "queue://"
|
||||
)
|
||||
|
||||
var ErrResourceNotFound = errors.New("resource not found")
|
||||
|
||||
type SempClient struct {
|
||||
client http.Client
|
||||
sempApiBaseUrl string
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
func (c SempClient) RequestWithoutBody(ctx context.Context, method string, url string) error {
|
||||
request, err := http.NewRequestWithContext(ctx, method, fmt.Sprintf("%s%s", c.sempApiBaseUrl, url), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := c.doRequest(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.parseResponseAsObject(response)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c SempClient) RequestWithBody(ctx context.Context, method string, url string, body any) error {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, method, fmt.Sprintf("%s%s", c.sempApiBaseUrl, url), bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := c.doRequest(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.parseResponseAsObject(response)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c SempClient) doRequest(request *http.Request) ([]byte, error) {
|
||||
request.SetBasicAuth(c.username, c.password)
|
||||
if request.Method != http.MethodGet {
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
response, err := c.client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
rawBody, err := io.ReadAll(response.Body)
|
||||
if err != nil || (response.StatusCode != http.StatusOK && response.StatusCode != http.StatusBadRequest) {
|
||||
return nil, fmt.Errorf("request to %v failes with status %v (%v), response:\n%s", response.StatusCode, response.Status, request.URL, rawBody)
|
||||
}
|
||||
if _, err := io.Copy(io.Discard, response.Body); err != nil {
|
||||
return nil, fmt.Errorf("response processing error for call to %v", request.URL)
|
||||
}
|
||||
return rawBody, nil
|
||||
}
|
||||
|
||||
func (c SempClient) parseResponseAsObject(dataResponse []byte) (map[string]any, error) {
|
||||
data := map[string]any{}
|
||||
err := json.Unmarshal(dataResponse, &data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse response:\n%s", dataResponse)
|
||||
}
|
||||
rawData, ok := data["data"]
|
||||
if ok {
|
||||
data, _ = rawData.(map[string]any)
|
||||
return data, nil
|
||||
} else {
|
||||
metadata, ok := data["meta"]
|
||||
if ok {
|
||||
data, _ = metadata.(map[string]any)
|
||||
if data["responseCode"].(float64) == http.StatusOK {
|
||||
// http-delete
|
||||
return nil, nil
|
||||
}
|
||||
description := data["error"].(map[string]interface{})["description"].(string)
|
||||
status := data["error"].(map[string]interface{})["status"].(string)
|
||||
if status == "NOT_FOUND" {
|
||||
// resource not found
|
||||
return nil, fmt.Errorf("request failed - description: %v, status: %v, %w", description, status, ErrResourceNotFound)
|
||||
}
|
||||
return nil, fmt.Errorf("request failed - description: %v, status: %v", description, status)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("could not parse response:\n%s", dataResponse)
|
||||
}
|
||||
|
||||
// SolaceContainer wraps a testcontainers docker container instance of solace.
|
||||
//
|
||||
// The container must be terminated by calling:
|
||||
// solaceContainer.Terminate(ctx)
|
||||
type SolaceContainer struct {
|
||||
testcontainers.Container
|
||||
AmqpConnectionString string
|
||||
sempClient SempClient
|
||||
}
|
||||
|
||||
// NewSolaceContainer starts a container and waits until it is ready to be used.
|
||||
func NewSolaceContainer(ctx context.Context) (*SolaceContainer, error) {
|
||||
|
||||
env := make(map[string]string)
|
||||
env["username_admin_globalaccesslevel"] = "admin"
|
||||
env["username_admin_password"] = "admin"
|
||||
|
||||
// Start docker container
|
||||
request := testcontainers.ContainerRequest{
|
||||
Image: "solace/solace-pubsub-standard:10.8",
|
||||
ExposedPorts: []string{"5672/tcp", "8080/tcp"},
|
||||
SkipReaper: true,
|
||||
AutoRemove: true,
|
||||
ShmSize: 1024 * 1024 * 1024, // 1 GB,
|
||||
Env: env,
|
||||
WaitingFor: wait.ForLog("Running pre-startup checks:").
|
||||
WithStartupTimeout(90 * time.Second),
|
||||
}
|
||||
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||||
ContainerRequest: request,
|
||||
Started: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract host and port information
|
||||
host, err := container.Host(ctx)
|
||||
if err != nil {
|
||||
_ = container.Terminate(ctx)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
amqpPort, err := container.MappedPort(ctx, "5672")
|
||||
if err != nil {
|
||||
_ = container.Terminate(ctx)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sempPort, err := container.MappedPort(ctx, "8080")
|
||||
if err != nil {
|
||||
_ = container.Terminate(ctx)
|
||||
return nil, err
|
||||
}
|
||||
log.AuditLogger.Info("UI Port: " + sempPort.Port())
|
||||
|
||||
// Construct connection strings
|
||||
amqpConnectionString := fmt.Sprintf("amqp://%s:%s/", host, amqpPort.Port())
|
||||
sempApiBaseUrl := fmt.Sprintf("http://%s:%s/SEMP/v2", host, sempPort.Port())
|
||||
|
||||
// Construct SEMP client
|
||||
sempClient := SempClient{client: http.Client{}, sempApiBaseUrl: sempApiBaseUrl, username: "admin", password: "admin"}
|
||||
|
||||
// Poll queue endpoint until solace is ready to interact
|
||||
solaceStarting := true
|
||||
for solaceStarting {
|
||||
err := sempClient.RequestWithoutBody(
|
||||
ctx,
|
||||
"GET",
|
||||
"/config/msgVpns/default/queues/test",
|
||||
)
|
||||
if err != nil && strings.Contains(err.Error(), "NOT_FOUND") {
|
||||
solaceStarting = false
|
||||
}
|
||||
time.Sleep(1000 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Return container object
|
||||
return &SolaceContainer{
|
||||
Container: container,
|
||||
AmqpConnectionString: amqpConnectionString,
|
||||
sempClient: sempClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// QueueCreate creates a queue with the given name.
|
||||
func (c SolaceContainer) QueueCreate(ctx context.Context, queueName string) error {
|
||||
|
||||
// Construct parameters
|
||||
var queueConfig = make(map[string]any)
|
||||
queueConfig["accessType"] = "non-exclusive"
|
||||
queueConfig["egressEnabled"] = true
|
||||
queueConfig["ingressEnabled"] = true
|
||||
queueConfig["permission"] = "consume"
|
||||
queueConfig["queueName"] = queueName
|
||||
queueConfig["maxBindCount"] = 100
|
||||
|
||||
// Create the queue
|
||||
err := c.sempClient.RequestWithBody(
|
||||
ctx,
|
||||
"POST",
|
||||
"/config/msgVpns/default/queues",
|
||||
queueConfig)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// QueueExists checks if a queue with the given name exists.
|
||||
func (c SolaceContainer) QueueExists(ctx context.Context, queueName string) (bool, error) {
|
||||
|
||||
// Check if exists
|
||||
err := c.sempClient.RequestWithoutBody(
|
||||
ctx,
|
||||
"GET",
|
||||
fmt.Sprintf("/config/msgVpns/default/queues/%s", queueName),
|
||||
)
|
||||
// Check if response contains "NOT_FOUND" string indicating that the queue doesn't exist
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "NOT_FOUND") {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Return technical errors
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Return queue exists
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// QueueDeleteIfExists deletes the queue with the given name if it exists.
|
||||
func (c SolaceContainer) QueueDeleteIfExists(ctx context.Context, queueName string) error {
|
||||
|
||||
// Check if queue exists
|
||||
exists, err := c.QueueExists(ctx, queueName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete if exists
|
||||
if exists {
|
||||
err := c.sempClient.RequestWithoutBody(
|
||||
ctx,
|
||||
"DELETE",
|
||||
fmt.Sprintf("/config/msgVpns/default/queues/%s", queueName),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TopicSubscriptionCreate creates a topic subscription for a (underlying) queue.
|
||||
//
|
||||
// Parameters:
|
||||
// * ctx - the context object
|
||||
// * queueName - the name of the queue where the topic(s) should be subscribed
|
||||
// * topicName - the name of the topic with optional wildcards (e.g. "organizations/org-*")
|
||||
func (c SolaceContainer) TopicSubscriptionCreate(ctx context.Context, queueName string, topicName string) error {
|
||||
|
||||
// Construct url and parameters
|
||||
url := fmt.Sprintf("/config/msgVpns/default/queues/%s/subscriptions", queueName)
|
||||
|
||||
subscriptionConfig := make(map[string]any)
|
||||
subscriptionConfig["subscriptionTopic"] = topicName
|
||||
|
||||
// Create the subscription
|
||||
err := c.sempClient.RequestWithBody(ctx, "POST", url, subscriptionConfig)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c SolaceContainer) NewAmqpConnection(ctx context.Context) (*amqp.Conn, error) {
|
||||
return amqp.Dial(ctx, c.AmqpConnectionString, nil)
|
||||
}
|
||||
|
||||
// ValidateTopicName checks that topicName and topicSubscriptionTopicPattern are valid and compatible
|
||||
// Solace topic name constraints can be found here:
|
||||
// https://docs.solace.com/Messaging/SMF-Topics.htm
|
||||
func (c SolaceContainer) ValidateTopicName(topicSubscriptionTopicPattern string, topicName string) error {
|
||||
// Cut off the topic:// prefix
|
||||
var name string
|
||||
if strings.HasPrefix(topicName, "topic://") {
|
||||
name = topicName[len("topic://"):]
|
||||
} else {
|
||||
name = topicName
|
||||
}
|
||||
|
||||
// Check input
|
||||
if topicSubscriptionTopicPattern == "" {
|
||||
return errors.New("topicSubscriptionTopicPattern is empty")
|
||||
}
|
||||
if name == "" {
|
||||
return errors.New("topicName is empty")
|
||||
}
|
||||
|
||||
// Check topic name
|
||||
allowedTopicCharacters, err := regexp.Compile(`[0-9A-Za-z-.]+(?:/[0-9A-Za-z-.]+)+|[0-9A-Za-z-.]+`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !allowedTopicCharacters.MatchString(name) {
|
||||
return errors.New("invalid topic name")
|
||||
}
|
||||
|
||||
// Check topic subscription topic pattern
|
||||
allowedTopicSubscriptionCharacters, err := regexp.Compile(
|
||||
`(?:(?:[0-9A-Za-z-.]+|[0-9A-Za-z-.]*\*)(?:/(?:[0-9A-Za-z-.]+|[0-9A-Za-z-.]*\*))+|(?:[0-9A-Za-z-.]+|[0-9A-Za-z-.]*\*)|/>)|>`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !allowedTopicSubscriptionCharacters.MatchString(topicSubscriptionTopicPattern) {
|
||||
return errors.New("invalid topic subscription name")
|
||||
}
|
||||
|
||||
// Check compatibility
|
||||
subscriptionIndex := 0
|
||||
var expectedNextCharacter uint8 = 0
|
||||
var nextError error
|
||||
for i := 0; i < len(name); i++ {
|
||||
if expectedNextCharacter != 0 {
|
||||
if expectedNextCharacter != name[i] {
|
||||
return nextError
|
||||
} else {
|
||||
expectedNextCharacter = 0
|
||||
nextError = nil
|
||||
}
|
||||
}
|
||||
|
||||
switch topicSubscriptionTopicPattern[subscriptionIndex] {
|
||||
case '*':
|
||||
if name[i] == '/' {
|
||||
expectedNextCharacter = '/'
|
||||
nextError = fmt.Errorf("invalid character '/' at index %d", i)
|
||||
subscriptionIndex++
|
||||
}
|
||||
case '/':
|
||||
if name[i] != '/' {
|
||||
return fmt.Errorf("expected character '/', got %c at index %d", name[i], i)
|
||||
}
|
||||
subscriptionIndex++
|
||||
case '>':
|
||||
// everything is allowed
|
||||
break
|
||||
default:
|
||||
if name[i] != topicSubscriptionTopicPattern[subscriptionIndex] {
|
||||
return fmt.Errorf(
|
||||
"expected character %c, got %c at index %d",
|
||||
topicSubscriptionTopicPattern[subscriptionIndex],
|
||||
name[i],
|
||||
i,
|
||||
)
|
||||
} else {
|
||||
subscriptionIndex++
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NextMessageFromQueue returns the next message from the queue.
|
||||
// It is important that the topic subscription matches the topic name.
|
||||
// Otherwise, no message is returned and the test will fail by exceeding the timeout.
|
||||
func (c SolaceContainer) NextMessageFromQueue(
|
||||
ctx context.Context,
|
||||
queueName string,
|
||||
accept bool,
|
||||
) (*amqp.Message, error) {
|
||||
|
||||
return c.NextMessage(ctx, fmt.Sprintf("queue://%s", queueName), accept)
|
||||
}
|
||||
|
||||
func (c SolaceContainer) NextMessage(ctx context.Context, target string, accept bool) (*amqp.Message, error) {
|
||||
if !strings.HasPrefix(target, AmqpTopicPrefix) && !strings.HasPrefix(target, AmqpQueuePrefix) {
|
||||
return nil, fmt.Errorf(
|
||||
"solace receive: target %q name lacks mandatory prefix %q, %q",
|
||||
target,
|
||||
AmqpTopicPrefix,
|
||||
AmqpQueuePrefix,
|
||||
)
|
||||
}
|
||||
|
||||
connection, err := c.NewAmqpConnection(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session, err := connection.NewSession(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
receiver, err := session.NewReceiver(ctx, target, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
message, err := receiver.Receive(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if accept {
|
||||
err := receiver.AcceptMessage(ctx, message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return message, nil
|
||||
}
|
||||
|
||||
func (c SolaceContainer) Stop() {
|
||||
_ = c.Terminate(context.Background())
|
||||
}
|
||||
|
||||
func (c SolaceContainer) StopOnError() {
|
||||
if r := recover(); r != nil {
|
||||
c.Stop()
|
||||
}
|
||||
}
|
||||
45
audit/utils/sequence_generator.go
Normal file
45
audit/utils/sequence_generator.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package utils
|
||||
|
||||
import "sync"
|
||||
|
||||
// SequenceNumberGenerator can be used to generate increasing numbers.
|
||||
type SequenceNumberGenerator interface {
|
||||
|
||||
// Next returns the next number
|
||||
Next() uint64
|
||||
|
||||
// Revert can be used to decrease the number (e.g. in case of an error)
|
||||
Revert()
|
||||
}
|
||||
|
||||
// DefaultSequenceNumberGenerator is a mutex protected implementation of SequenceNumberGenerator
|
||||
type DefaultSequenceNumberGenerator struct {
|
||||
sequenceNumber uint64
|
||||
sequenceNumberLock sync.Mutex
|
||||
}
|
||||
|
||||
// NewDefaultSequenceNumberGenerator returns an instance of DefaultSequenceNumberGenerator as pointer
|
||||
// of SequenceNumberGenerator.
|
||||
func NewDefaultSequenceNumberGenerator() *SequenceNumberGenerator {
|
||||
var generator SequenceNumberGenerator = &DefaultSequenceNumberGenerator{
|
||||
sequenceNumber: 0,
|
||||
sequenceNumberLock: sync.Mutex{},
|
||||
}
|
||||
return &generator
|
||||
}
|
||||
|
||||
// Next implements SequenceNumberGenerator.Next
|
||||
func (g *DefaultSequenceNumberGenerator) Next() uint64 {
|
||||
g.sequenceNumberLock.Lock()
|
||||
defer g.sequenceNumberLock.Unlock()
|
||||
next := g.sequenceNumber
|
||||
g.sequenceNumber += 1
|
||||
return next
|
||||
}
|
||||
|
||||
// Revert implements SequenceNumberGenerator.Revert
|
||||
func (g *DefaultSequenceNumberGenerator) Revert() {
|
||||
g.sequenceNumberLock.Lock()
|
||||
defer g.sequenceNumberLock.Unlock()
|
||||
g.sequenceNumber -= 1
|
||||
}
|
||||
22
audit/utils/sequence_generator_test.go
Normal file
22
audit/utils/sequence_generator_test.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_DefaultSequenceNumberGenerator(t *testing.T) {
|
||||
|
||||
t.Run("next", func(t *testing.T) {
|
||||
var sequenceGenerator = NewDefaultSequenceNumberGenerator()
|
||||
assert.Equal(t, uint64(0), (*sequenceGenerator).Next())
|
||||
})
|
||||
|
||||
t.Run("revert", func(t *testing.T) {
|
||||
var sequenceGenerator = NewDefaultSequenceNumberGenerator()
|
||||
assert.Equal(t, uint64(0), (*sequenceGenerator).Next())
|
||||
assert.Equal(t, uint64(1), (*sequenceGenerator).Next())
|
||||
(*sequenceGenerator).Revert()
|
||||
assert.Equal(t, uint64(1), (*sequenceGenerator).Next())
|
||||
})
|
||||
}
|
||||
2
buf.lock
Normal file
2
buf.lock
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Generated by buf. DO NOT EDIT.
|
||||
version: v1
|
||||
1954
gen/go/audit/v1/audit_event.pb.go
Normal file
1954
gen/go/audit/v1/audit_event.pb.go
Normal file
File diff suppressed because it is too large
Load diff
2209
gen/go/audit/v1/audit_event.pb.validate.go
Normal file
2209
gen/go/audit/v1/audit_event.pb.validate.go
Normal file
File diff suppressed because it is too large
Load diff
543
gen/go/audit/v1/routable_event.pb.go
Normal file
543
gen/go/audit/v1/routable_event.pb.go
Normal file
|
|
@ -0,0 +1,543 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.1
|
||||
// protoc (unknown)
|
||||
// source: audit/v1/routable_event.proto
|
||||
|
||||
package auditV1
|
||||
|
||||
import (
|
||||
_ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type Visibility int32
|
||||
|
||||
const (
|
||||
Visibility_VISIBILITY_UNSPECIFIED Visibility = 0
|
||||
// Will be routed to customer data sinks
|
||||
Visibility_VISIBILITY_PUBLIC Visibility = 1
|
||||
// Will NOT be routed to customer data sinks
|
||||
Visibility_VISIBILITY_PRIVATE Visibility = 2
|
||||
)
|
||||
|
||||
// Enum value maps for Visibility.
|
||||
var (
|
||||
Visibility_name = map[int32]string{
|
||||
0: "VISIBILITY_UNSPECIFIED",
|
||||
1: "VISIBILITY_PUBLIC",
|
||||
2: "VISIBILITY_PRIVATE",
|
||||
}
|
||||
Visibility_value = map[string]int32{
|
||||
"VISIBILITY_UNSPECIFIED": 0,
|
||||
"VISIBILITY_PUBLIC": 1,
|
||||
"VISIBILITY_PRIVATE": 2,
|
||||
}
|
||||
)
|
||||
|
||||
func (x Visibility) Enum() *Visibility {
|
||||
p := new(Visibility)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x Visibility) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (Visibility) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_audit_v1_routable_event_proto_enumTypes[0].Descriptor()
|
||||
}
|
||||
|
||||
func (Visibility) Type() protoreflect.EnumType {
|
||||
return &file_audit_v1_routable_event_proto_enumTypes[0]
|
||||
}
|
||||
|
||||
func (x Visibility) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Visibility.Descriptor instead.
|
||||
func (Visibility) EnumDescriptor() ([]byte, []int) {
|
||||
return file_audit_v1_routable_event_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
// Identifier of an object.
|
||||
//
|
||||
// For system events, the nil UUID must be used: 00000000-0000-0000-0000-000000000000.
|
||||
type ObjectIdentifier struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// Identifier of the respective entity (e.g. Identifier of an organization)
|
||||
//
|
||||
// Required: true
|
||||
Identifier string `protobuf:"bytes,1,opt,name=identifier,proto3" json:"identifier,omitempty"`
|
||||
// Entity data type relevant for routing - one of the list of supported object types.
|
||||
//
|
||||
// Required: true
|
||||
Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"`
|
||||
}
|
||||
|
||||
func (x *ObjectIdentifier) Reset() {
|
||||
*x = ObjectIdentifier{}
|
||||
mi := &file_audit_v1_routable_event_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ObjectIdentifier) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ObjectIdentifier) ProtoMessage() {}
|
||||
|
||||
func (x *ObjectIdentifier) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_audit_v1_routable_event_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ObjectIdentifier.ProtoReflect.Descriptor instead.
|
||||
func (*ObjectIdentifier) Descriptor() ([]byte, []int) {
|
||||
return file_audit_v1_routable_event_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *ObjectIdentifier) GetIdentifier() string {
|
||||
if x != nil {
|
||||
return x.Identifier
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ObjectIdentifier) GetType() string {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type EncryptedData struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// Encrypted serialized protobuf content (the actual audit event)
|
||||
//
|
||||
// Required: true
|
||||
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
|
||||
// Name of the protobuf type
|
||||
//
|
||||
// Required: true
|
||||
ProtobufType string `protobuf:"bytes,2,opt,name=protobuf_type,json=protobufType,proto3" json:"protobuf_type,omitempty"`
|
||||
// The password taken to derive the encryption key from
|
||||
//
|
||||
// Required: true
|
||||
EncryptedPassword string `protobuf:"bytes,3,opt,name=encrypted_password,json=encryptedPassword,proto3" json:"encrypted_password,omitempty"`
|
||||
// Version of the encrypted key
|
||||
//
|
||||
// Required: true
|
||||
KeyVersion int32 `protobuf:"varint,4,opt,name=key_version,json=keyVersion,proto3" json:"key_version,omitempty"`
|
||||
}
|
||||
|
||||
func (x *EncryptedData) Reset() {
|
||||
*x = EncryptedData{}
|
||||
mi := &file_audit_v1_routable_event_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *EncryptedData) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*EncryptedData) ProtoMessage() {}
|
||||
|
||||
func (x *EncryptedData) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_audit_v1_routable_event_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use EncryptedData.ProtoReflect.Descriptor instead.
|
||||
func (*EncryptedData) Descriptor() ([]byte, []int) {
|
||||
return file_audit_v1_routable_event_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *EncryptedData) GetData() []byte {
|
||||
if x != nil {
|
||||
return x.Data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *EncryptedData) GetProtobufType() string {
|
||||
if x != nil {
|
||||
return x.ProtobufType
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *EncryptedData) GetEncryptedPassword() string {
|
||||
if x != nil {
|
||||
return x.EncryptedPassword
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *EncryptedData) GetKeyVersion() int32 {
|
||||
if x != nil {
|
||||
return x.KeyVersion
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type UnencryptedData struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// Unencrypted serialized protobuf content (the actual audit event)
|
||||
//
|
||||
// Required: true
|
||||
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
|
||||
// Name of the protobuf type
|
||||
//
|
||||
// Required: true
|
||||
ProtobufType string `protobuf:"bytes,2,opt,name=protobuf_type,json=protobufType,proto3" json:"protobuf_type,omitempty"`
|
||||
}
|
||||
|
||||
func (x *UnencryptedData) Reset() {
|
||||
*x = UnencryptedData{}
|
||||
mi := &file_audit_v1_routable_event_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *UnencryptedData) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*UnencryptedData) ProtoMessage() {}
|
||||
|
||||
func (x *UnencryptedData) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_audit_v1_routable_event_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use UnencryptedData.ProtoReflect.Descriptor instead.
|
||||
func (*UnencryptedData) Descriptor() ([]byte, []int) {
|
||||
return file_audit_v1_routable_event_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *UnencryptedData) GetData() []byte {
|
||||
if x != nil {
|
||||
return x.Data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *UnencryptedData) GetProtobufType() string {
|
||||
if x != nil {
|
||||
return x.ProtobufType
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type RoutableAuditEvent struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// Functional event name with pattern
|
||||
//
|
||||
// Format: stackit.<product>.<version>.<type-chain>.<operation>
|
||||
// Where:
|
||||
//
|
||||
// Product: The name of the service in lowercase
|
||||
// Version: Optional API version
|
||||
// Type-Chain: Chained path to object
|
||||
// Operation: The name of the operation in lowercase
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// "stackit.resource-manager.v1.organizations.create"
|
||||
// "stackit.authorization.v1.projects.volumes.create"
|
||||
// "stackit.authorization.v2alpha.projects.volumes.create"
|
||||
// "stackit.authorization.v2.folders.move"
|
||||
// "stackit.resource-manager.health"
|
||||
//
|
||||
// Required: true
|
||||
OperationName string `protobuf:"bytes,1,opt,name=operation_name,json=operationName,proto3" json:"operation_name,omitempty"`
|
||||
// Visibility relevant for differentiating between internal and public events
|
||||
//
|
||||
// Required: true
|
||||
Visibility Visibility `protobuf:"varint,2,opt,name=visibility,proto3,enum=audit.v1.Visibility" json:"visibility,omitempty"`
|
||||
// Identifier the audit log event refers to.
|
||||
//
|
||||
// System events, will not be routed to the end-user.
|
||||
//
|
||||
// Required: true
|
||||
ObjectIdentifier *ObjectIdentifier `protobuf:"bytes,3,opt,name=object_identifier,json=objectIdentifier,proto3" json:"object_identifier,omitempty"`
|
||||
// The actual audit event is transferred in one of the attributes below
|
||||
//
|
||||
// Required: true
|
||||
//
|
||||
// Types that are assignable to Data:
|
||||
//
|
||||
// *RoutableAuditEvent_UnencryptedData
|
||||
// *RoutableAuditEvent_EncryptedData
|
||||
Data isRoutableAuditEvent_Data `protobuf_oneof:"data"`
|
||||
}
|
||||
|
||||
func (x *RoutableAuditEvent) Reset() {
|
||||
*x = RoutableAuditEvent{}
|
||||
mi := &file_audit_v1_routable_event_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RoutableAuditEvent) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RoutableAuditEvent) ProtoMessage() {}
|
||||
|
||||
func (x *RoutableAuditEvent) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_audit_v1_routable_event_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RoutableAuditEvent.ProtoReflect.Descriptor instead.
|
||||
func (*RoutableAuditEvent) Descriptor() ([]byte, []int) {
|
||||
return file_audit_v1_routable_event_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *RoutableAuditEvent) GetOperationName() string {
|
||||
if x != nil {
|
||||
return x.OperationName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RoutableAuditEvent) GetVisibility() Visibility {
|
||||
if x != nil {
|
||||
return x.Visibility
|
||||
}
|
||||
return Visibility_VISIBILITY_UNSPECIFIED
|
||||
}
|
||||
|
||||
func (x *RoutableAuditEvent) GetObjectIdentifier() *ObjectIdentifier {
|
||||
if x != nil {
|
||||
return x.ObjectIdentifier
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *RoutableAuditEvent) GetData() isRoutableAuditEvent_Data {
|
||||
if m != nil {
|
||||
return m.Data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *RoutableAuditEvent) GetUnencryptedData() *UnencryptedData {
|
||||
if x, ok := x.GetData().(*RoutableAuditEvent_UnencryptedData); ok {
|
||||
return x.UnencryptedData
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *RoutableAuditEvent) GetEncryptedData() *EncryptedData {
|
||||
if x, ok := x.GetData().(*RoutableAuditEvent_EncryptedData); ok {
|
||||
return x.EncryptedData
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isRoutableAuditEvent_Data interface {
|
||||
isRoutableAuditEvent_Data()
|
||||
}
|
||||
|
||||
type RoutableAuditEvent_UnencryptedData struct {
|
||||
UnencryptedData *UnencryptedData `protobuf:"bytes,4,opt,name=unencrypted_data,json=unencryptedData,proto3,oneof"`
|
||||
}
|
||||
|
||||
type RoutableAuditEvent_EncryptedData struct {
|
||||
EncryptedData *EncryptedData `protobuf:"bytes,5,opt,name=encrypted_data,json=encryptedData,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*RoutableAuditEvent_UnencryptedData) isRoutableAuditEvent_Data() {}
|
||||
|
||||
func (*RoutableAuditEvent_EncryptedData) isRoutableAuditEvent_Data() {}
|
||||
|
||||
var File_audit_v1_routable_event_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_audit_v1_routable_event_proto_rawDesc = []byte{
|
||||
0x0a, 0x1d, 0x61, 0x75, 0x64, 0x69, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x61,
|
||||
0x62, 0x6c, 0x65, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
|
||||
0x08, 0x61, 0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x1a, 0x1b, 0x62, 0x75, 0x66, 0x2f, 0x76,
|
||||
0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65,
|
||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x5f, 0x0a, 0x10, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74,
|
||||
0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x0a, 0x69, 0x64,
|
||||
0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0b,
|
||||
0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0a, 0x69, 0x64, 0x65,
|
||||
0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x1e, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18,
|
||||
0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0xc8, 0x01, 0x01, 0x72, 0x02, 0x10,
|
||||
0x01, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xc5, 0x01, 0x0a, 0x0d, 0x45, 0x6e, 0x63, 0x72,
|
||||
0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x12, 0x1e, 0x0a, 0x04, 0x64, 0x61, 0x74,
|
||||
0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xba, 0x48, 0x07, 0xc8, 0x01, 0x01, 0x7a,
|
||||
0x02, 0x10, 0x01, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2f, 0x0a, 0x0d, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
||||
0x42, 0x0a, 0xba, 0x48, 0x07, 0xc8, 0x01, 0x01, 0x72, 0x02, 0x10, 0x01, 0x52, 0x0c, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x54, 0x79, 0x70, 0x65, 0x12, 0x39, 0x0a, 0x12, 0x65, 0x6e,
|
||||
0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
|
||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0xc8, 0x01, 0x01, 0x72, 0x02,
|
||||
0x10, 0x01, 0x52, 0x11, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x50, 0x61, 0x73,
|
||||
0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x28, 0x0a, 0x0b, 0x6b, 0x65, 0x79, 0x5f, 0x76, 0x65, 0x72,
|
||||
0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x42, 0x07, 0xba, 0x48, 0x04, 0x1a,
|
||||
0x02, 0x28, 0x01, 0x52, 0x0a, 0x6b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22,
|
||||
0x62, 0x0a, 0x0f, 0x55, 0x6e, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61,
|
||||
0x74, 0x61, 0x12, 0x1e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c,
|
||||
0x42, 0x0a, 0xba, 0x48, 0x07, 0xc8, 0x01, 0x01, 0x7a, 0x02, 0x10, 0x01, 0x52, 0x04, 0x64, 0x61,
|
||||
0x74, 0x61, 0x12, 0x2f, 0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x5f, 0x74,
|
||||
0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0xc8, 0x01,
|
||||
0x01, 0x72, 0x02, 0x10, 0x01, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x54,
|
||||
0x79, 0x70, 0x65, 0x22, 0xb5, 0x03, 0x0a, 0x12, 0x52, 0x6f, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65,
|
||||
0x41, 0x75, 0x64, 0x69, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x72, 0x0a, 0x0e, 0x6f, 0x70,
|
||||
0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x42, 0x4b, 0xba, 0x48, 0x48, 0xc8, 0x01, 0x01, 0x72, 0x43, 0x32, 0x41, 0x5e, 0x73,
|
||||
0x74, 0x61, 0x63, 0x6b, 0x69, 0x74, 0x5c, 0x2e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x2d,
|
||||
0x5d, 0x2b, 0x5c, 0x2e, 0x28, 0x3f, 0x3a, 0x76, 0x5b, 0x30, 0x2d, 0x39, 0x5d, 0x2b, 0x5c, 0x2e,
|
||||
0x29, 0x3f, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x2d, 0x2e, 0x5d, 0x2b,
|
||||
0x5c, 0x2e, 0x29, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x2d, 0x5d, 0x2b, 0x24, 0x52,
|
||||
0x0d, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x41,
|
||||
0x0a, 0x0a, 0x76, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01,
|
||||
0x28, 0x0e, 0x32, 0x14, 0x2e, 0x61, 0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x69,
|
||||
0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01,
|
||||
0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x0a, 0x76, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74,
|
||||
0x79, 0x12, 0x4f, 0x0a, 0x11, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x65, 0x6e,
|
||||
0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61,
|
||||
0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64,
|
||||
0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01,
|
||||
0x52, 0x10, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69,
|
||||
0x65, 0x72, 0x12, 0x46, 0x0a, 0x10, 0x75, 0x6e, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65,
|
||||
0x64, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61,
|
||||
0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70,
|
||||
0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x48, 0x00, 0x52, 0x0f, 0x75, 0x6e, 0x65, 0x6e, 0x63,
|
||||
0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x12, 0x40, 0x0a, 0x0e, 0x65, 0x6e,
|
||||
0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01,
|
||||
0x28, 0x0b, 0x32, 0x17, 0x2e, 0x61, 0x75, 0x64, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e,
|
||||
0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x48, 0x00, 0x52, 0x0d, 0x65,
|
||||
0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x42, 0x0d, 0x0a, 0x04,
|
||||
0x64, 0x61, 0x74, 0x61, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x2a, 0x57, 0x0a, 0x0a, 0x56,
|
||||
0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x12, 0x1a, 0x0a, 0x16, 0x56, 0x49, 0x53,
|
||||
0x49, 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46,
|
||||
0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, 0x56, 0x49, 0x53, 0x49, 0x42, 0x49, 0x4c,
|
||||
0x49, 0x54, 0x59, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12,
|
||||
0x56, 0x49, 0x53, 0x49, 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, 0x5f, 0x50, 0x52, 0x49, 0x56, 0x41,
|
||||
0x54, 0x45, 0x10, 0x02, 0x42, 0x31, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x73, 0x63, 0x68, 0x77,
|
||||
0x61, 0x72, 0x7a, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x69, 0x74, 0x2e, 0x61, 0x75, 0x64, 0x69,
|
||||
0x74, 0x2e, 0x76, 0x31, 0x50, 0x01, 0x5a, 0x0f, 0x2e, 0x2f, 0x61, 0x75, 0x64, 0x69, 0x74, 0x3b,
|
||||
0x61, 0x75, 0x64, 0x69, 0x74, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_audit_v1_routable_event_proto_rawDescOnce sync.Once
|
||||
file_audit_v1_routable_event_proto_rawDescData = file_audit_v1_routable_event_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_audit_v1_routable_event_proto_rawDescGZIP() []byte {
|
||||
file_audit_v1_routable_event_proto_rawDescOnce.Do(func() {
|
||||
file_audit_v1_routable_event_proto_rawDescData = protoimpl.X.CompressGZIP(file_audit_v1_routable_event_proto_rawDescData)
|
||||
})
|
||||
return file_audit_v1_routable_event_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_audit_v1_routable_event_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_audit_v1_routable_event_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
|
||||
var file_audit_v1_routable_event_proto_goTypes = []any{
|
||||
(Visibility)(0), // 0: audit.v1.Visibility
|
||||
(*ObjectIdentifier)(nil), // 1: audit.v1.ObjectIdentifier
|
||||
(*EncryptedData)(nil), // 2: audit.v1.EncryptedData
|
||||
(*UnencryptedData)(nil), // 3: audit.v1.UnencryptedData
|
||||
(*RoutableAuditEvent)(nil), // 4: audit.v1.RoutableAuditEvent
|
||||
}
|
||||
var file_audit_v1_routable_event_proto_depIdxs = []int32{
|
||||
0, // 0: audit.v1.RoutableAuditEvent.visibility:type_name -> audit.v1.Visibility
|
||||
1, // 1: audit.v1.RoutableAuditEvent.object_identifier:type_name -> audit.v1.ObjectIdentifier
|
||||
3, // 2: audit.v1.RoutableAuditEvent.unencrypted_data:type_name -> audit.v1.UnencryptedData
|
||||
2, // 3: audit.v1.RoutableAuditEvent.encrypted_data:type_name -> audit.v1.EncryptedData
|
||||
4, // [4:4] is the sub-list for method output_type
|
||||
4, // [4:4] is the sub-list for method input_type
|
||||
4, // [4:4] is the sub-list for extension type_name
|
||||
4, // [4:4] is the sub-list for extension extendee
|
||||
0, // [0:4] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_audit_v1_routable_event_proto_init() }
|
||||
func file_audit_v1_routable_event_proto_init() {
|
||||
if File_audit_v1_routable_event_proto != nil {
|
||||
return
|
||||
}
|
||||
file_audit_v1_routable_event_proto_msgTypes[3].OneofWrappers = []any{
|
||||
(*RoutableAuditEvent_UnencryptedData)(nil),
|
||||
(*RoutableAuditEvent_EncryptedData)(nil),
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_audit_v1_routable_event_proto_rawDesc,
|
||||
NumEnums: 1,
|
||||
NumMessages: 4,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_audit_v1_routable_event_proto_goTypes,
|
||||
DependencyIndexes: file_audit_v1_routable_event_proto_depIdxs,
|
||||
EnumInfos: file_audit_v1_routable_event_proto_enumTypes,
|
||||
MessageInfos: file_audit_v1_routable_event_proto_msgTypes,
|
||||
}.Build()
|
||||
File_audit_v1_routable_event_proto = out.File
|
||||
file_audit_v1_routable_event_proto_rawDesc = nil
|
||||
file_audit_v1_routable_event_proto_goTypes = nil
|
||||
file_audit_v1_routable_event_proto_depIdxs = nil
|
||||
}
|
||||
574
gen/go/audit/v1/routable_event.pb.validate.go
Normal file
574
gen/go/audit/v1/routable_event.pb.validate.go
Normal file
|
|
@ -0,0 +1,574 @@
|
|||
// Code generated by protoc-gen-validate. DO NOT EDIT.
|
||||
// source: audit/v1/routable_event.proto
|
||||
|
||||
package auditV1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
)
|
||||
|
||||
// ensure the imports are used
|
||||
var (
|
||||
_ = bytes.MinRead
|
||||
_ = errors.New("")
|
||||
_ = fmt.Print
|
||||
_ = utf8.UTFMax
|
||||
_ = (*regexp.Regexp)(nil)
|
||||
_ = (*strings.Reader)(nil)
|
||||
_ = net.IPv4len
|
||||
_ = time.Duration(0)
|
||||
_ = (*url.URL)(nil)
|
||||
_ = (*mail.Address)(nil)
|
||||
_ = anypb.Any{}
|
||||
_ = sort.Sort
|
||||
)
|
||||
|
||||
// Validate checks the field values on ObjectIdentifier with the rules defined
|
||||
// in the proto definition for this message. If any rules are violated, the
|
||||
// first error encountered is returned, or nil if there are no violations.
|
||||
func (m *ObjectIdentifier) Validate() error {
|
||||
return m.validate(false)
|
||||
}
|
||||
|
||||
// ValidateAll checks the field values on ObjectIdentifier with the rules
|
||||
// defined in the proto definition for this message. If any rules are
|
||||
// violated, the result is a list of violation errors wrapped in
|
||||
// ObjectIdentifierMultiError, or nil if none found.
|
||||
func (m *ObjectIdentifier) ValidateAll() error {
|
||||
return m.validate(true)
|
||||
}
|
||||
|
||||
func (m *ObjectIdentifier) validate(all bool) error {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var errors []error
|
||||
|
||||
// no validation rules for Identifier
|
||||
|
||||
// no validation rules for Type
|
||||
|
||||
if len(errors) > 0 {
|
||||
return ObjectIdentifierMultiError(errors)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ObjectIdentifierMultiError is an error wrapping multiple validation errors
|
||||
// returned by ObjectIdentifier.ValidateAll() if the designated constraints
|
||||
// aren't met.
|
||||
type ObjectIdentifierMultiError []error
|
||||
|
||||
// Error returns a concatenation of all the error messages it wraps.
|
||||
func (m ObjectIdentifierMultiError) Error() string {
|
||||
var msgs []string
|
||||
for _, err := range m {
|
||||
msgs = append(msgs, err.Error())
|
||||
}
|
||||
return strings.Join(msgs, "; ")
|
||||
}
|
||||
|
||||
// AllErrors returns a list of validation violation errors.
|
||||
func (m ObjectIdentifierMultiError) AllErrors() []error { return m }
|
||||
|
||||
// ObjectIdentifierValidationError is the validation error returned by
|
||||
// ObjectIdentifier.Validate if the designated constraints aren't met.
|
||||
type ObjectIdentifierValidationError struct {
|
||||
field string
|
||||
reason string
|
||||
cause error
|
||||
key bool
|
||||
}
|
||||
|
||||
// Field function returns field value.
|
||||
func (e ObjectIdentifierValidationError) Field() string { return e.field }
|
||||
|
||||
// Reason function returns reason value.
|
||||
func (e ObjectIdentifierValidationError) Reason() string { return e.reason }
|
||||
|
||||
// Cause function returns cause value.
|
||||
func (e ObjectIdentifierValidationError) Cause() error { return e.cause }
|
||||
|
||||
// Key function returns key value.
|
||||
func (e ObjectIdentifierValidationError) Key() bool { return e.key }
|
||||
|
||||
// ErrorName returns error name.
|
||||
func (e ObjectIdentifierValidationError) ErrorName() string { return "ObjectIdentifierValidationError" }
|
||||
|
||||
// Error satisfies the builtin error interface
|
||||
func (e ObjectIdentifierValidationError) Error() string {
|
||||
cause := ""
|
||||
if e.cause != nil {
|
||||
cause = fmt.Sprintf(" | caused by: %v", e.cause)
|
||||
}
|
||||
|
||||
key := ""
|
||||
if e.key {
|
||||
key = "key for "
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"invalid %sObjectIdentifier.%s: %s%s",
|
||||
key,
|
||||
e.field,
|
||||
e.reason,
|
||||
cause)
|
||||
}
|
||||
|
||||
var _ error = ObjectIdentifierValidationError{}
|
||||
|
||||
var _ interface {
|
||||
Field() string
|
||||
Reason() string
|
||||
Key() bool
|
||||
Cause() error
|
||||
ErrorName() string
|
||||
} = ObjectIdentifierValidationError{}
|
||||
|
||||
// Validate checks the field values on EncryptedData with the rules defined in
|
||||
// the proto definition for this message. If any rules are violated, the first
|
||||
// error encountered is returned, or nil if there are no violations.
|
||||
func (m *EncryptedData) Validate() error {
|
||||
return m.validate(false)
|
||||
}
|
||||
|
||||
// ValidateAll checks the field values on EncryptedData with the rules defined
|
||||
// in the proto definition for this message. If any rules are violated, the
|
||||
// result is a list of violation errors wrapped in EncryptedDataMultiError, or
|
||||
// nil if none found.
|
||||
func (m *EncryptedData) ValidateAll() error {
|
||||
return m.validate(true)
|
||||
}
|
||||
|
||||
func (m *EncryptedData) validate(all bool) error {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var errors []error
|
||||
|
||||
// no validation rules for Data
|
||||
|
||||
// no validation rules for ProtobufType
|
||||
|
||||
// no validation rules for EncryptedPassword
|
||||
|
||||
// no validation rules for KeyVersion
|
||||
|
||||
if len(errors) > 0 {
|
||||
return EncryptedDataMultiError(errors)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncryptedDataMultiError is an error wrapping multiple validation errors
|
||||
// returned by EncryptedData.ValidateAll() if the designated constraints
|
||||
// aren't met.
|
||||
type EncryptedDataMultiError []error
|
||||
|
||||
// Error returns a concatenation of all the error messages it wraps.
|
||||
func (m EncryptedDataMultiError) Error() string {
|
||||
var msgs []string
|
||||
for _, err := range m {
|
||||
msgs = append(msgs, err.Error())
|
||||
}
|
||||
return strings.Join(msgs, "; ")
|
||||
}
|
||||
|
||||
// AllErrors returns a list of validation violation errors.
|
||||
func (m EncryptedDataMultiError) AllErrors() []error { return m }
|
||||
|
||||
// EncryptedDataValidationError is the validation error returned by
|
||||
// EncryptedData.Validate if the designated constraints aren't met.
|
||||
type EncryptedDataValidationError struct {
|
||||
field string
|
||||
reason string
|
||||
cause error
|
||||
key bool
|
||||
}
|
||||
|
||||
// Field function returns field value.
|
||||
func (e EncryptedDataValidationError) Field() string { return e.field }
|
||||
|
||||
// Reason function returns reason value.
|
||||
func (e EncryptedDataValidationError) Reason() string { return e.reason }
|
||||
|
||||
// Cause function returns cause value.
|
||||
func (e EncryptedDataValidationError) Cause() error { return e.cause }
|
||||
|
||||
// Key function returns key value.
|
||||
func (e EncryptedDataValidationError) Key() bool { return e.key }
|
||||
|
||||
// ErrorName returns error name.
|
||||
func (e EncryptedDataValidationError) ErrorName() string { return "EncryptedDataValidationError" }
|
||||
|
||||
// Error satisfies the builtin error interface
|
||||
func (e EncryptedDataValidationError) Error() string {
|
||||
cause := ""
|
||||
if e.cause != nil {
|
||||
cause = fmt.Sprintf(" | caused by: %v", e.cause)
|
||||
}
|
||||
|
||||
key := ""
|
||||
if e.key {
|
||||
key = "key for "
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"invalid %sEncryptedData.%s: %s%s",
|
||||
key,
|
||||
e.field,
|
||||
e.reason,
|
||||
cause)
|
||||
}
|
||||
|
||||
var _ error = EncryptedDataValidationError{}
|
||||
|
||||
var _ interface {
|
||||
Field() string
|
||||
Reason() string
|
||||
Key() bool
|
||||
Cause() error
|
||||
ErrorName() string
|
||||
} = EncryptedDataValidationError{}
|
||||
|
||||
// Validate checks the field values on UnencryptedData with the rules defined
|
||||
// in the proto definition for this message. If any rules are violated, the
|
||||
// first error encountered is returned, or nil if there are no violations.
|
||||
func (m *UnencryptedData) Validate() error {
|
||||
return m.validate(false)
|
||||
}
|
||||
|
||||
// ValidateAll checks the field values on UnencryptedData with the rules
|
||||
// defined in the proto definition for this message. If any rules are
|
||||
// violated, the result is a list of violation errors wrapped in
|
||||
// UnencryptedDataMultiError, or nil if none found.
|
||||
func (m *UnencryptedData) ValidateAll() error {
|
||||
return m.validate(true)
|
||||
}
|
||||
|
||||
func (m *UnencryptedData) validate(all bool) error {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var errors []error
|
||||
|
||||
// no validation rules for Data
|
||||
|
||||
// no validation rules for ProtobufType
|
||||
|
||||
if len(errors) > 0 {
|
||||
return UnencryptedDataMultiError(errors)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnencryptedDataMultiError is an error wrapping multiple validation errors
|
||||
// returned by UnencryptedData.ValidateAll() if the designated constraints
|
||||
// aren't met.
|
||||
type UnencryptedDataMultiError []error
|
||||
|
||||
// Error returns a concatenation of all the error messages it wraps.
|
||||
func (m UnencryptedDataMultiError) Error() string {
|
||||
var msgs []string
|
||||
for _, err := range m {
|
||||
msgs = append(msgs, err.Error())
|
||||
}
|
||||
return strings.Join(msgs, "; ")
|
||||
}
|
||||
|
||||
// AllErrors returns a list of validation violation errors.
|
||||
func (m UnencryptedDataMultiError) AllErrors() []error { return m }
|
||||
|
||||
// UnencryptedDataValidationError is the validation error returned by
|
||||
// UnencryptedData.Validate if the designated constraints aren't met.
|
||||
type UnencryptedDataValidationError struct {
|
||||
field string
|
||||
reason string
|
||||
cause error
|
||||
key bool
|
||||
}
|
||||
|
||||
// Field function returns field value.
|
||||
func (e UnencryptedDataValidationError) Field() string { return e.field }
|
||||
|
||||
// Reason function returns reason value.
|
||||
func (e UnencryptedDataValidationError) Reason() string { return e.reason }
|
||||
|
||||
// Cause function returns cause value.
|
||||
func (e UnencryptedDataValidationError) Cause() error { return e.cause }
|
||||
|
||||
// Key function returns key value.
|
||||
func (e UnencryptedDataValidationError) Key() bool { return e.key }
|
||||
|
||||
// ErrorName returns error name.
|
||||
func (e UnencryptedDataValidationError) ErrorName() string { return "UnencryptedDataValidationError" }
|
||||
|
||||
// Error satisfies the builtin error interface
|
||||
func (e UnencryptedDataValidationError) Error() string {
|
||||
cause := ""
|
||||
if e.cause != nil {
|
||||
cause = fmt.Sprintf(" | caused by: %v", e.cause)
|
||||
}
|
||||
|
||||
key := ""
|
||||
if e.key {
|
||||
key = "key for "
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"invalid %sUnencryptedData.%s: %s%s",
|
||||
key,
|
||||
e.field,
|
||||
e.reason,
|
||||
cause)
|
||||
}
|
||||
|
||||
var _ error = UnencryptedDataValidationError{}
|
||||
|
||||
var _ interface {
|
||||
Field() string
|
||||
Reason() string
|
||||
Key() bool
|
||||
Cause() error
|
||||
ErrorName() string
|
||||
} = UnencryptedDataValidationError{}
|
||||
|
||||
// Validate checks the field values on RoutableAuditEvent with the rules
|
||||
// defined in the proto definition for this message. If any rules are
|
||||
// violated, the first error encountered is returned, or nil if there are no violations.
|
||||
func (m *RoutableAuditEvent) Validate() error {
|
||||
return m.validate(false)
|
||||
}
|
||||
|
||||
// ValidateAll checks the field values on RoutableAuditEvent with the rules
|
||||
// defined in the proto definition for this message. If any rules are
|
||||
// violated, the result is a list of violation errors wrapped in
|
||||
// RoutableAuditEventMultiError, or nil if none found.
|
||||
func (m *RoutableAuditEvent) ValidateAll() error {
|
||||
return m.validate(true)
|
||||
}
|
||||
|
||||
func (m *RoutableAuditEvent) validate(all bool) error {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var errors []error
|
||||
|
||||
// no validation rules for OperationName
|
||||
|
||||
// no validation rules for Visibility
|
||||
|
||||
if all {
|
||||
switch v := interface{}(m.GetObjectIdentifier()).(type) {
|
||||
case interface{ ValidateAll() error }:
|
||||
if err := v.ValidateAll(); err != nil {
|
||||
errors = append(errors, RoutableAuditEventValidationError{
|
||||
field: "ObjectIdentifier",
|
||||
reason: "embedded message failed validation",
|
||||
cause: err,
|
||||
})
|
||||
}
|
||||
case interface{ Validate() error }:
|
||||
if err := v.Validate(); err != nil {
|
||||
errors = append(errors, RoutableAuditEventValidationError{
|
||||
field: "ObjectIdentifier",
|
||||
reason: "embedded message failed validation",
|
||||
cause: err,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if v, ok := interface{}(m.GetObjectIdentifier()).(interface{ Validate() error }); ok {
|
||||
if err := v.Validate(); err != nil {
|
||||
return RoutableAuditEventValidationError{
|
||||
field: "ObjectIdentifier",
|
||||
reason: "embedded message failed validation",
|
||||
cause: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch v := m.Data.(type) {
|
||||
case *RoutableAuditEvent_UnencryptedData:
|
||||
if v == nil {
|
||||
err := RoutableAuditEventValidationError{
|
||||
field: "Data",
|
||||
reason: "oneof value cannot be a typed-nil",
|
||||
}
|
||||
if !all {
|
||||
return err
|
||||
}
|
||||
errors = append(errors, err)
|
||||
}
|
||||
|
||||
if all {
|
||||
switch v := interface{}(m.GetUnencryptedData()).(type) {
|
||||
case interface{ ValidateAll() error }:
|
||||
if err := v.ValidateAll(); err != nil {
|
||||
errors = append(errors, RoutableAuditEventValidationError{
|
||||
field: "UnencryptedData",
|
||||
reason: "embedded message failed validation",
|
||||
cause: err,
|
||||
})
|
||||
}
|
||||
case interface{ Validate() error }:
|
||||
if err := v.Validate(); err != nil {
|
||||
errors = append(errors, RoutableAuditEventValidationError{
|
||||
field: "UnencryptedData",
|
||||
reason: "embedded message failed validation",
|
||||
cause: err,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if v, ok := interface{}(m.GetUnencryptedData()).(interface{ Validate() error }); ok {
|
||||
if err := v.Validate(); err != nil {
|
||||
return RoutableAuditEventValidationError{
|
||||
field: "UnencryptedData",
|
||||
reason: "embedded message failed validation",
|
||||
cause: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case *RoutableAuditEvent_EncryptedData:
|
||||
if v == nil {
|
||||
err := RoutableAuditEventValidationError{
|
||||
field: "Data",
|
||||
reason: "oneof value cannot be a typed-nil",
|
||||
}
|
||||
if !all {
|
||||
return err
|
||||
}
|
||||
errors = append(errors, err)
|
||||
}
|
||||
|
||||
if all {
|
||||
switch v := interface{}(m.GetEncryptedData()).(type) {
|
||||
case interface{ ValidateAll() error }:
|
||||
if err := v.ValidateAll(); err != nil {
|
||||
errors = append(errors, RoutableAuditEventValidationError{
|
||||
field: "EncryptedData",
|
||||
reason: "embedded message failed validation",
|
||||
cause: err,
|
||||
})
|
||||
}
|
||||
case interface{ Validate() error }:
|
||||
if err := v.Validate(); err != nil {
|
||||
errors = append(errors, RoutableAuditEventValidationError{
|
||||
field: "EncryptedData",
|
||||
reason: "embedded message failed validation",
|
||||
cause: err,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if v, ok := interface{}(m.GetEncryptedData()).(interface{ Validate() error }); ok {
|
||||
if err := v.Validate(); err != nil {
|
||||
return RoutableAuditEventValidationError{
|
||||
field: "EncryptedData",
|
||||
reason: "embedded message failed validation",
|
||||
cause: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
_ = v // ensures v is used
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return RoutableAuditEventMultiError(errors)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RoutableAuditEventMultiError is an error wrapping multiple validation errors
|
||||
// returned by RoutableAuditEvent.ValidateAll() if the designated constraints
|
||||
// aren't met.
|
||||
type RoutableAuditEventMultiError []error
|
||||
|
||||
// Error returns a concatenation of all the error messages it wraps.
|
||||
func (m RoutableAuditEventMultiError) Error() string {
|
||||
var msgs []string
|
||||
for _, err := range m {
|
||||
msgs = append(msgs, err.Error())
|
||||
}
|
||||
return strings.Join(msgs, "; ")
|
||||
}
|
||||
|
||||
// AllErrors returns a list of validation violation errors.
|
||||
func (m RoutableAuditEventMultiError) AllErrors() []error { return m }
|
||||
|
||||
// RoutableAuditEventValidationError is the validation error returned by
|
||||
// RoutableAuditEvent.Validate if the designated constraints aren't met.
|
||||
type RoutableAuditEventValidationError struct {
|
||||
field string
|
||||
reason string
|
||||
cause error
|
||||
key bool
|
||||
}
|
||||
|
||||
// Field function returns field value.
|
||||
func (e RoutableAuditEventValidationError) Field() string { return e.field }
|
||||
|
||||
// Reason function returns reason value.
|
||||
func (e RoutableAuditEventValidationError) Reason() string { return e.reason }
|
||||
|
||||
// Cause function returns cause value.
|
||||
func (e RoutableAuditEventValidationError) Cause() error { return e.cause }
|
||||
|
||||
// Key function returns key value.
|
||||
func (e RoutableAuditEventValidationError) Key() bool { return e.key }
|
||||
|
||||
// ErrorName returns error name.
|
||||
func (e RoutableAuditEventValidationError) ErrorName() string {
|
||||
return "RoutableAuditEventValidationError"
|
||||
}
|
||||
|
||||
// Error satisfies the builtin error interface
|
||||
func (e RoutableAuditEventValidationError) Error() string {
|
||||
cause := ""
|
||||
if e.cause != nil {
|
||||
cause = fmt.Sprintf(" | caused by: %v", e.cause)
|
||||
}
|
||||
|
||||
key := ""
|
||||
if e.key {
|
||||
key = "key for "
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"invalid %sRoutableAuditEvent.%s: %s%s",
|
||||
key,
|
||||
e.field,
|
||||
e.reason,
|
||||
cause)
|
||||
}
|
||||
|
||||
var _ error = RoutableAuditEventValidationError{}
|
||||
|
||||
var _ interface {
|
||||
Field() string
|
||||
Reason() string
|
||||
Key() bool
|
||||
Cause() error
|
||||
ErrorName() string
|
||||
} = RoutableAuditEventValidationError{}
|
||||
73
go.mod
Normal file
73
go.mod
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
module dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git
|
||||
|
||||
go 1.23.2
|
||||
|
||||
require (
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.1-20240920164238-5a7b106cbb87.1
|
||||
github.com/Azure/go-amqp v1.2.0
|
||||
github.com/bufbuild/protovalidate-go v0.7.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/testcontainers/testcontainers-go v0.34.0
|
||||
go.opentelemetry.io/otel v1.31.0
|
||||
go.opentelemetry.io/otel/trace v1.31.0
|
||||
google.golang.org/protobuf v1.35.1
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/containerd/containerd v1.7.18 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/docker v27.1.1+incompatible // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/cel-go v0.21.0 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/sys/sequential v0.5.0 // indirect
|
||||
github.com/moby/sys/user v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.31.0 // indirect
|
||||
golang.org/x/crypto v0.24.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
223
go.sum
Normal file
223
go.sum
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.1-20240920164238-5a7b106cbb87.1 h1:9wP6ZZYWnF2Z0TxmII7m3XNykxnP4/w8oXeth6ekcRI=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.1-20240920164238-5a7b106cbb87.1/go.mod h1:Duw/9JoXkXIydyASnLYIiufkzySThoqavOsF+IihqvM=
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-amqp v1.2.0 h1:NNyfN3/cRszWzMvjmm64yaPZDHX/2DJkowv8Ub9y01I=
|
||||
github.com/Azure/go-amqp v1.2.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||
github.com/bufbuild/protovalidate-go v0.7.2 h1:UuvKyZHl5p7u3ztEjtRtqtDxOjRKX5VUOgKFq6p6ETk=
|
||||
github.com/bufbuild/protovalidate-go v0.7.2/go.mod h1:PHV5pFuWlRzdDW02/cmVyNzdiQ+RNNwo7idGxdzS7o4=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao=
|
||||
github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
|
||||
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI=
|
||||
github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
|
||||
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
|
||||
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
|
||||
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
|
||||
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
|
||||
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
|
||||
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/testcontainers/testcontainers-go v0.34.0 h1:5fbgF0vIN5u+nD3IWabQwRybuB4GY8G2HHgCkbMzMHo=
|
||||
github.com/testcontainers/testcontainers-go v0.34.0/go.mod h1:6P/kMkQe8yqPHfPWNulFGdFHTD8HB2vLq/231xY2iPQ=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
|
||||
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
||||
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
|
||||
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
|
||||
go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
|
||||
go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
|
||||
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
|
||||
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda h1:b6F6WIV4xHHD0FA4oIyzU6mHWg2WI2X1RBehwa5QN38=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda/go.mod h1:AHcE/gZH76Bk/ROZhQphlRoWo5xKDEtz3eVEO1LfA8c=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
|
||||
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
27
log/log.go
Normal file
27
log/log.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
var AuditLogger Logger = SlogLogger{logger: slog.Default()}
|
||||
|
||||
type Logger interface {
|
||||
Debug(msg string, err ...error)
|
||||
Info(msg string, err ...error)
|
||||
Warn(msg string, err ...error)
|
||||
Error(msg string, err ...error)
|
||||
}
|
||||
|
||||
func wrapErr(err []error) error {
|
||||
var e error
|
||||
if len(err) == 0 {
|
||||
e = nil
|
||||
} else if len(err) == 1 {
|
||||
e = err[0]
|
||||
} else {
|
||||
e = errors.Join(err...)
|
||||
}
|
||||
return e
|
||||
}
|
||||
40
log/log_test.go
Normal file
40
log/log_test.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_DefaultLogger(t *testing.T) {
|
||||
t.Run("debug", func(t *testing.T) {
|
||||
AuditLogger.Debug("debug message")
|
||||
})
|
||||
|
||||
t.Run("debug with error details", func(t *testing.T) {
|
||||
AuditLogger.Debug("debug message", errors.New("custom error"))
|
||||
})
|
||||
|
||||
t.Run("info", func(t *testing.T) {
|
||||
AuditLogger.Info("info message")
|
||||
})
|
||||
|
||||
t.Run("info with error details", func(t *testing.T) {
|
||||
AuditLogger.Info("info message", errors.New("custom error"))
|
||||
})
|
||||
|
||||
t.Run("warn", func(t *testing.T) {
|
||||
AuditLogger.Warn("warn message")
|
||||
})
|
||||
|
||||
t.Run("warn with error details", func(t *testing.T) {
|
||||
AuditLogger.Warn("warn message", errors.New("custom error"))
|
||||
})
|
||||
|
||||
t.Run("error", func(t *testing.T) {
|
||||
AuditLogger.Error("error message")
|
||||
})
|
||||
|
||||
t.Run("error with error details", func(t *testing.T) {
|
||||
AuditLogger.Error("error message", errors.New("custom error"))
|
||||
})
|
||||
}
|
||||
35
log/slog.go
Normal file
35
log/slog.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package log
|
||||
|
||||
import "log/slog"
|
||||
|
||||
type SlogLogger struct {
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func UseSlogAuditLogger(logger *slog.Logger) {
|
||||
AuditLogger = SlogLogger{logger: logger}
|
||||
}
|
||||
|
||||
func (s SlogLogger) Debug(msg string, err ...error) {
|
||||
s.logger.Debug(msg, s.getWrappedError(err))
|
||||
}
|
||||
|
||||
func (s SlogLogger) Info(msg string, err ...error) {
|
||||
s.logger.Info(msg, s.getWrappedError(err))
|
||||
}
|
||||
|
||||
func (s SlogLogger) Warn(msg string, err ...error) {
|
||||
s.logger.Warn(msg, s.getWrappedError(err))
|
||||
}
|
||||
|
||||
func (s SlogLogger) Error(msg string, err ...error) {
|
||||
s.logger.Error(msg, s.getWrappedError(err))
|
||||
}
|
||||
|
||||
func (s SlogLogger) getWrappedError(err []error) slog.Attr {
|
||||
var wrappedErr slog.Attr
|
||||
if err != nil {
|
||||
wrappedErr = slog.Any("error", wrapErr(err))
|
||||
}
|
||||
return wrappedErr
|
||||
}
|
||||
43
log/slog_test.go
Normal file
43
log/slog_test.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_SlogLogger(t *testing.T) {
|
||||
UseSlogAuditLogger(slog.Default())
|
||||
|
||||
t.Run("debug", func(t *testing.T) {
|
||||
AuditLogger.Debug("debug message")
|
||||
})
|
||||
|
||||
t.Run("debug with error details", func(t *testing.T) {
|
||||
AuditLogger.Debug("debug message", errors.New("custom error"))
|
||||
})
|
||||
|
||||
t.Run("info", func(t *testing.T) {
|
||||
AuditLogger.Info("info message")
|
||||
})
|
||||
|
||||
t.Run("info with error details", func(t *testing.T) {
|
||||
AuditLogger.Info("info message", errors.New("custom error"))
|
||||
})
|
||||
|
||||
t.Run("warn", func(t *testing.T) {
|
||||
AuditLogger.Warn("warn message")
|
||||
})
|
||||
|
||||
t.Run("warn with error details", func(t *testing.T) {
|
||||
AuditLogger.Warn("warn message", errors.New("custom error"))
|
||||
})
|
||||
|
||||
t.Run("error", func(t *testing.T) {
|
||||
AuditLogger.Error("error message")
|
||||
})
|
||||
|
||||
t.Run("error with error details", func(t *testing.T) {
|
||||
AuditLogger.Error("error message", errors.New("custom error"))
|
||||
})
|
||||
}
|
||||
25
log/zerolog.go
Normal file
25
log/zerolog.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package log
|
||||
|
||||
import "github.com/rs/zerolog/log"
|
||||
|
||||
type ZeroLogLogger struct{}
|
||||
|
||||
func UseZerologAuditLogger() {
|
||||
AuditLogger = ZeroLogLogger{}
|
||||
}
|
||||
|
||||
func (l ZeroLogLogger) Debug(msg string, err ...error) {
|
||||
log.Debug().Err(wrapErr(err)).Msg(msg)
|
||||
}
|
||||
|
||||
func (l ZeroLogLogger) Info(msg string, err ...error) {
|
||||
log.Info().Err(wrapErr(err)).Msg(msg)
|
||||
}
|
||||
|
||||
func (l ZeroLogLogger) Warn(msg string, err ...error) {
|
||||
log.Warn().Err(wrapErr(err)).Msg(msg)
|
||||
}
|
||||
|
||||
func (l ZeroLogLogger) Error(msg string, err ...error) {
|
||||
log.Error().Err(wrapErr(err)).Msg(msg)
|
||||
}
|
||||
42
log/zerolog_test.go
Normal file
42
log/zerolog_test.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_ZerologLogger(t *testing.T) {
|
||||
UseZerologAuditLogger()
|
||||
|
||||
t.Run("debug", func(t *testing.T) {
|
||||
AuditLogger.Debug("debug message")
|
||||
})
|
||||
|
||||
t.Run("debug with error details", func(t *testing.T) {
|
||||
AuditLogger.Debug("debug message", errors.New("custom error"))
|
||||
})
|
||||
|
||||
t.Run("info", func(t *testing.T) {
|
||||
AuditLogger.Info("info message")
|
||||
})
|
||||
|
||||
t.Run("info with error details", func(t *testing.T) {
|
||||
AuditLogger.Info("info message", errors.New("custom error"))
|
||||
})
|
||||
|
||||
t.Run("warn", func(t *testing.T) {
|
||||
AuditLogger.Warn("warn message")
|
||||
})
|
||||
|
||||
t.Run("warn with error details", func(t *testing.T) {
|
||||
AuditLogger.Warn("warn message", errors.New("custom error"))
|
||||
})
|
||||
|
||||
t.Run("error", func(t *testing.T) {
|
||||
AuditLogger.Error("error message")
|
||||
})
|
||||
|
||||
t.Run("error with error details", func(t *testing.T) {
|
||||
AuditLogger.Error("error message", errors.New("custom error"))
|
||||
})
|
||||
}
|
||||
632
proto/audit/v1/audit_event.proto
Normal file
632
proto/audit/v1/audit_event.proto
Normal file
|
|
@ -0,0 +1,632 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package audit.v1;
|
||||
|
||||
import "buf/validate/validate.proto";
|
||||
import "google/protobuf/struct.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "google/protobuf/wrappers.proto";
|
||||
|
||||
option go_package = "./audit;auditV1";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "com.schwarz.stackit.audit.v1";
|
||||
|
||||
// The audit log entry can be used to record an incident in the audit log.
|
||||
message AuditLogEntry {
|
||||
// The resource name of the log to which this log entry belongs.
|
||||
//
|
||||
// Format: <pluralType>/<identifier>/logs/<eventType>
|
||||
// Where:
|
||||
// Plural-Types: One from the list of supported ObjectType as plural
|
||||
// Event-Types: admin-activity, system-event, policy-denied, data-access
|
||||
//
|
||||
// Examples:
|
||||
// "projects/00b0f972-59ff-48f2-a4f9-29c57b75c2fa/logs/admin-activity"
|
||||
// "billing-accounts/00b0f972-59ff-48f2-a4f9-29c57b75c2fa/logs/admin-activity"
|
||||
//
|
||||
// Required: true
|
||||
string log_name = 1 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.pattern = "^[a-z-]+/[a-z0-9-]+/logs/(?:admin-activity|system-event|policy-denied|data-access)$"
|
||||
];
|
||||
|
||||
// The log entry payload, which is always an AuditLog for STACKIT Audit Log events.
|
||||
//
|
||||
// Required: true
|
||||
AuditLog proto_payload = 2 [(buf.validate.field).required = true];
|
||||
|
||||
// A unique identifier for the log entry.
|
||||
// Is used to check completeness of audit events over time.
|
||||
//
|
||||
// Format: <unix-timestamp>/<region-zone>/<worker-id>/<sequence-number>
|
||||
// Where:
|
||||
// Unix-Timestamp: A UTC unix timestamp in seconds is expected
|
||||
// Region-Zone: The region and (optional) zone id. If both, separated with a - (dash)
|
||||
// Worker-Id: The ID of the K8s Pod, Service-Instance, etc (must be unique for a sending service)
|
||||
// Sequence-Number: Increasing number, representing the message offset per Worker-Id
|
||||
// If the Worker-Id changes, the sequence-number has to be reset to 0.
|
||||
//
|
||||
// Examples:
|
||||
// "1721899117/eu01/319a7fb9-edd2-46c6-953a-a724bb377c61/8792726390909855142"
|
||||
// "1721899117/eu01-m/319a7fb9-edd2-46c6-953a-a724bb377c61/8792726390909855142"
|
||||
//
|
||||
// Required: true
|
||||
string insert_id = 3 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.pattern = "^[0-9]+/[a-z0-9-]+/[a-z0-9-]+/[0-9]+$"
|
||||
];
|
||||
|
||||
// A set of user-defined (key, value) data that provides additional
|
||||
// information about the log entry.
|
||||
//
|
||||
// Required: false
|
||||
map<string, string> labels = 4;
|
||||
|
||||
// Correlate multiple audit logs by setting the same id
|
||||
//
|
||||
// Required: false
|
||||
optional string correlation_id = 5 [
|
||||
(buf.validate.field).string.min_len = 1,
|
||||
(buf.validate.field).string.max_len = 255
|
||||
];
|
||||
|
||||
// The time the event described by the log entry occurred.
|
||||
//
|
||||
// Required: true
|
||||
google.protobuf.Timestamp timestamp = 6 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).timestamp.lt_now = true
|
||||
];
|
||||
|
||||
// The severity of the log entry.
|
||||
//
|
||||
// Required: true
|
||||
LogSeverity severity = 7 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).enum.defined_only = true
|
||||
];
|
||||
|
||||
// Customer set W3C conform trace parent header:
|
||||
// https://www.w3.org/TR/trace-context/#traceparent-header
|
||||
//
|
||||
// Format: <version>-<trace-id>-<parent-id>-<trace-flags>
|
||||
//
|
||||
// Examples:
|
||||
// "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
|
||||
//
|
||||
// Required: false
|
||||
optional string trace_parent = 8 [(buf.validate.field).string.pattern = "^[0-9]+-[a-z0-9]+-[a-z0-9]+-[0-9]+$"];
|
||||
|
||||
// Customer set W3C conform trace state header:
|
||||
// https://www.w3.org/TR/trace-context/#tracestate-header
|
||||
//
|
||||
// Format: <key1>=<value1>[,<keyN>=<valueN>]
|
||||
//
|
||||
// Examples:
|
||||
// "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"
|
||||
//
|
||||
// Required: false
|
||||
optional string trace_state = 9;
|
||||
}
|
||||
|
||||
// The severity of the event described in a log entry, expressed as one of the
|
||||
// standard severity levels listed below.
|
||||
enum LogSeverity {
|
||||
LOG_SEVERITY_UNSPECIFIED = 0;
|
||||
|
||||
// The log entry has no assigned severity level.
|
||||
LOG_SEVERITY_DEFAULT = 100;
|
||||
|
||||
// Debug or trace information.
|
||||
LOG_SEVERITY_DEBUG = 200;
|
||||
|
||||
// Routine information, such as ongoing status or performance.
|
||||
LOG_SEVERITY_INFO = 300;
|
||||
|
||||
// Normal but significant events, such as start up, shut down, or
|
||||
// a configuration change.
|
||||
LOG_SEVERITY_NOTICE = 400;
|
||||
|
||||
// Warning events might cause problems.
|
||||
LOG_SEVERITY_WARNING = 500;
|
||||
|
||||
// Error events are likely to cause problems.
|
||||
LOG_SEVERITY_ERROR = 600;
|
||||
|
||||
// Critical events cause more severe problems or outages.
|
||||
LOG_SEVERITY_CRITICAL = 700;
|
||||
|
||||
// A person must take an action immediately.
|
||||
LOG_SEVERITY_ALERT = 800;
|
||||
|
||||
// One or more systems are unusable.
|
||||
LOG_SEVERITY_EMERGENCY = 900;
|
||||
}
|
||||
|
||||
// Common audit log format for STACKIT API operations.
|
||||
message AuditLog {
|
||||
// The name of the API service performing the operation.
|
||||
//
|
||||
// Examples:
|
||||
// "resource-manager"
|
||||
//
|
||||
// Required: true
|
||||
string service_name = 1 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.min_len = 1
|
||||
];
|
||||
|
||||
// The name of the service method or operation.
|
||||
//
|
||||
// Format: stackit.<product>.<version>.<type-chain>.<operation>
|
||||
// Where:
|
||||
// Product: The name of the service in lowercase
|
||||
// Version: Optional API version
|
||||
// Type-Chain: Chained path to object
|
||||
// Operation: The name of the operation in lowercase
|
||||
//
|
||||
// Examples:
|
||||
// "stackit.resource-manager.v1.organizations.create"
|
||||
// "stackit.authorization.v1.projects.volumes.create"
|
||||
// "stackit.authorization.v2alpha.projects.volumes.create"
|
||||
// "stackit.authorization.v2.folders.move"
|
||||
// "stackit.resource-manager.health"
|
||||
//
|
||||
// Required: true
|
||||
string operation_name = 2 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.pattern = "^stackit\\.[a-z0-9-]+\\.(?:v[0-9]+\\.)?(?:[a-z0-9-.]+\\.)?[a-z0-9-]+$",
|
||||
(buf.validate.field).string.min_len = 1,
|
||||
(buf.validate.field).string.max_len = 255
|
||||
];
|
||||
|
||||
// The resource or collection that is the target of the operation.
|
||||
// The name is a scheme-less URI, not including the API service name.
|
||||
//
|
||||
// Format: <pluralType>/<id>[/<details>]
|
||||
// Where:
|
||||
// Plural-Type: One from the list of supported ObjectType as plural
|
||||
// Id: The identifier of the object
|
||||
// Details: Optional "<key>/<id>" pairs
|
||||
//
|
||||
// Examples:
|
||||
// "organizations/40ab14ad-b7b0-4b1c-be41-5bc820a968d1"
|
||||
// "projects/7046e7b6-5ae9-441c-99fe-2cd28a5078ec/locations/_/instances/instance-20240723-174217"
|
||||
// "projects/7046e7b6-5ae9-441c-99fe-2cd28a5078ec/locations/sx-stoi01/instances/instance-20240723-174217"
|
||||
// "projects/dd7d1807-54e9-4426-8994-721758b5b554/locations/eu01/vms/b6851b4e-7a9d-4973-ab0f-a80a13ee3060/ports/78f8bad4-a291-4fa3-b07f-4a1985d3dbe8"
|
||||
// "projects/dd7d1807-54e9-4426-8994-721758b5b554/locations/eu01-m/vms/b6851b4e-7a9d-4973-ab0f-a80a13ee3060/ports/78f8bad4-a291-4fa3-b07f-4a1985d3dbe8"
|
||||
//
|
||||
// Required: true
|
||||
string resource_name = 3 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.pattern = "^[a-z]+/[a-z0-9-]+(?:/[a-z0-9-]+/[a-z0-9-_]+)*$",
|
||||
(buf.validate.field).string.min_len = 1,
|
||||
(buf.validate.field).string.max_len = 255
|
||||
];
|
||||
|
||||
// Authentication information.
|
||||
//
|
||||
// Required: true
|
||||
AuthenticationInfo authentication_info = 4 [(buf.validate.field).required = true];
|
||||
|
||||
// Authorization information. If there are multiple resources or permissions involved, then there is
|
||||
// one AuthorizationInfo element for each {resource, permission} tuple.
|
||||
//
|
||||
// Required: false
|
||||
repeated AuthorizationInfo authorization_info = 5;
|
||||
|
||||
// Metadata about the operation.
|
||||
//
|
||||
// Required: true
|
||||
RequestMetadata request_metadata = 6 [(buf.validate.field).required = true];
|
||||
|
||||
// The operation request. This may not include all request parameters,
|
||||
// such as those that are too large, privacy-sensitive, or duplicated
|
||||
// elsewhere in the log record.
|
||||
// It should never include user-generated data, such as file contents.
|
||||
//
|
||||
// Required: false
|
||||
optional google.protobuf.Struct request = 7;
|
||||
|
||||
// The status of the overall operation.
|
||||
//
|
||||
// Required: true
|
||||
ResponseMetadata response_metadata = 8 [(buf.validate.field).required = true];
|
||||
|
||||
// The operation response. This may not include all response elements,
|
||||
// such as those that are too large, privacy-sensitive, or duplicated
|
||||
// elsewhere in the log record.
|
||||
//
|
||||
// Required: false
|
||||
optional google.protobuf.Struct response = 9;
|
||||
|
||||
// Other service-specific data about the request, response, and other
|
||||
// information associated with the current audited event.
|
||||
//
|
||||
// Required: false
|
||||
optional google.protobuf.Struct metadata = 10;
|
||||
}
|
||||
|
||||
// Authentication information for the operation.
|
||||
message AuthenticationInfo {
|
||||
// STACKIT principal id
|
||||
//
|
||||
// Required: true
|
||||
string principal_id = 1 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.min_len = 1
|
||||
];
|
||||
|
||||
// The email address of the authenticated user.
|
||||
// Service accounts have email addresses that can be used.
|
||||
//
|
||||
// Required: true
|
||||
string principal_email = 2 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.min_len = 1,
|
||||
(buf.validate.field).string.max_len = 255
|
||||
];
|
||||
|
||||
// The name of the service account used to create or exchange
|
||||
// credentials for authenticating the service account making the request.
|
||||
//
|
||||
// Format: projects/<id>/service-accounts/<accountId>
|
||||
//
|
||||
// Examples:
|
||||
// "projects/29b2c56f-f712-4a9c-845b-f0907158e53c/service-accounts/a606dc68-8b97-421b-89a9-116bcbd004df"
|
||||
//
|
||||
// Required: false
|
||||
optional string service_account_name = 3 [(buf.validate.field).string.pattern = "^[a-z-]+/[a-z0-9-]+/service-accounts/[a-z0-9-]+$"];
|
||||
|
||||
// Identity delegation history of an authenticated service account that makes
|
||||
// the request. It contains information on the real authorities that try to
|
||||
// access STACKIT resources by delegating on a service account. When multiple
|
||||
// authorities present, they are guaranteed to be sorted based on the original
|
||||
// ordering of the identity delegation events.
|
||||
//
|
||||
// Required: false
|
||||
repeated ServiceAccountDelegationInfo service_account_delegation_info = 4;
|
||||
}
|
||||
|
||||
// Authorization information for the operation.
|
||||
message AuthorizationInfo {
|
||||
// The resource being accessed, as a REST-style string.
|
||||
//
|
||||
// Format: <pluralType>/<id>[/<details>]
|
||||
// Where:
|
||||
// Plural-Type: One from the list of supported ObjectType as plural
|
||||
// Id: The identifier of the object
|
||||
// Details: Optional "<key>/<id>" pairs
|
||||
//
|
||||
// Examples:
|
||||
// "organizations/40ab14ad-b7b0-4b1c-be41-5bc820a968d1"
|
||||
// "projects/7046e7b6-5ae9-441c-99fe-2cd28a5078ec/locations/_/instances/instance-20240723-174217"
|
||||
// "projects/7046e7b6-5ae9-441c-99fe-2cd28a5078ec/locations/eu01/instances/instance-20240723-174217"
|
||||
// "projects/7046e7b6-5ae9-441c-99fe-2cd28a5078ec/locations/eu01/vms/b6851b4e-7a9d-4973-ab0f-a80a13ee3060/ports/78f8bad4-a291-4fa3-b07f-4a1985d3dbe8"
|
||||
//
|
||||
// Required: true
|
||||
string resource = 1 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.pattern = "^[a-z]+/[a-z0-9-]+(?:/[a-z0-9-]+/[a-z0-9-_]+)*$"
|
||||
];
|
||||
|
||||
// The required IAM permission.
|
||||
//
|
||||
// Examples:
|
||||
// "resourcemanager.project.edit"
|
||||
//
|
||||
// Required: false
|
||||
optional string permission = 2 [(buf.validate.field).string.pattern = "^[a-z-]+(?:\\.[a-z-]+)*\\.[a-z-]+$"];
|
||||
|
||||
// IAM permission check result.
|
||||
//
|
||||
// Required: false
|
||||
optional bool granted = 3;
|
||||
}
|
||||
|
||||
// This message defines the standard attribute vocabulary for STACKIT APIs.
|
||||
//
|
||||
// An attribute is a piece of metadata that describes an activity on a network
|
||||
// service.
|
||||
message AttributeContext {
|
||||
// This message defines request authentication attributes. Terminology is
|
||||
// based on the JSON Web Token (JWT) standard, but the terms also
|
||||
// correlate to concepts in other standards.
|
||||
message Auth {
|
||||
// The authenticated principal. Reflects the issuer ("iss") and subject
|
||||
// ("sub") claims within a JWT.
|
||||
//
|
||||
// Format: <sub-claim>/<iss-claim>
|
||||
// Where:
|
||||
// Sub-Claim: Sub-Claim from JWT with `/` percent-encoded (url-encoded)
|
||||
// Issuer-Claim: Iss-Claim from JWT with `/` percent-encoded (url-encoded)
|
||||
//
|
||||
// Examples:
|
||||
// "stackit-resource-manager-dev/https%3A%2F%2Faccounts.dev.stackit.cloud"
|
||||
//
|
||||
// Required: true
|
||||
string principal = 1 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.pattern = "^[a-zA-Z0-9-%.]+/[a-zA-Z0-9-%.]+$"
|
||||
];
|
||||
|
||||
// The intended audience(s) for this authentication information. Reflects
|
||||
// the audience ("aud") claim within a JWT, typically the services intended
|
||||
// to receive the credential.
|
||||
//
|
||||
// Examples:
|
||||
// ["stackit-resource-manager-dev", "stackit", "api"]
|
||||
//
|
||||
// Required: false
|
||||
repeated string audiences = 2;
|
||||
|
||||
// Structured claims presented with the credential. JWTs include
|
||||
// {"key": <value>} pairs for standard and private claims.
|
||||
//
|
||||
// The following is a subset of the standard required and optional claims that should
|
||||
// typically be presented for a STACKIT JWT.
|
||||
// Don't add other claims to not leak internal or personal information:
|
||||
//
|
||||
// {
|
||||
// "aud": "stackit-resource-manager-dev",
|
||||
// "email": "max@mail.schwarz",
|
||||
// "iss": "https://api.dev.stackit.cloud",
|
||||
// "jti": "45a196e0-480f-4c34-a592-dc5db81c8c3a"
|
||||
// "sub": "cd94f01a-df2e-4456-902f-48f5e57f0b63"
|
||||
// }
|
||||
//
|
||||
// Required: true
|
||||
google.protobuf.Struct claims = 3 [(buf.validate.field).required = true];
|
||||
}
|
||||
|
||||
enum HttpMethod {
|
||||
HTTP_METHOD_UNSPECIFIED = 0;
|
||||
HTTP_METHOD_OTHER = 1;
|
||||
HTTP_METHOD_GET = 2;
|
||||
HTTP_METHOD_HEAD = 3;
|
||||
HTTP_METHOD_POST = 4;
|
||||
HTTP_METHOD_PUT = 5;
|
||||
HTTP_METHOD_DELETE = 6;
|
||||
HTTP_METHOD_CONNECT = 7;
|
||||
HTTP_METHOD_OPTIONS = 8;
|
||||
HTTP_METHOD_TRACE = 9;
|
||||
HTTP_METHOD_PATCH = 10;
|
||||
}
|
||||
|
||||
// This message defines attributes for an HTTP request. If the actual
|
||||
// request is not an HTTP request, the runtime system should try to map
|
||||
// the actual request to an equivalent HTTP request.
|
||||
message Request {
|
||||
// The unique ID for a request, which can be propagated to downstream
|
||||
// systems. The ID should have low probability of collision
|
||||
// within a single day for a specific service.
|
||||
//
|
||||
// More information can be found here: https://google.aip.dev/155
|
||||
//
|
||||
// Format: <idempotency-key>
|
||||
// Where:
|
||||
// Idempotency-key: Typically consists of a id + version
|
||||
//
|
||||
// Examples:
|
||||
// 5e3952a9-b628-4be6-ac61-b1c6eb4a110c/5
|
||||
//
|
||||
// Required: false
|
||||
optional string id = 1;
|
||||
|
||||
// The (HTTP) request method, such as `GET`, `POST`.
|
||||
//
|
||||
// Required: true
|
||||
HttpMethod method = 2 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).enum.defined_only = true
|
||||
];
|
||||
|
||||
// The (HTTP) request headers / gRPC metadata. If multiple headers share the same key, they
|
||||
// must be merged according to the HTTP spec. All header keys must be
|
||||
// lowercased, because HTTP header keys are case-insensitive.
|
||||
//
|
||||
// Internal IP-Addresses have to be removed (e.g. in x-forwarded-xxx headers).
|
||||
//
|
||||
// Required: true
|
||||
map<string, string> headers = 3 [(buf.validate.field).required = true];
|
||||
|
||||
// The gRPC / HTTP URL path.
|
||||
//
|
||||
// Required: true
|
||||
string path = 4 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.min_len = 1,
|
||||
(buf.validate.field).string.max_len = 255
|
||||
];
|
||||
|
||||
// The HTTP request `Host` header value.
|
||||
//
|
||||
// Required: true
|
||||
string host = 5 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.min_len = 1
|
||||
];
|
||||
|
||||
// The URL scheme, such as `http`, `https` or `gRPC`.
|
||||
//
|
||||
// Required: true
|
||||
string scheme = 6 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.min_len = 1
|
||||
];
|
||||
|
||||
// The HTTP URL query in the format of "name1=value1&name2=value2", as it
|
||||
// appears in the first line of the HTTP request.
|
||||
// The input should be escaped to not contain any special characters.
|
||||
//
|
||||
// Required: false
|
||||
optional string query = 7;
|
||||
|
||||
// The timestamp when the `destination` service receives the first byte of
|
||||
// the request.
|
||||
//
|
||||
// Required: true
|
||||
google.protobuf.Timestamp time = 8 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).timestamp.lt_now = true
|
||||
];
|
||||
|
||||
// The network protocol used with the request, such as "http/1.1",
|
||||
// "spdy/3", "h2", "h2c", "webrtc", "tcp", "udp", "quic". See
|
||||
// https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
|
||||
// for details.
|
||||
//
|
||||
// Required: true
|
||||
string protocol = 9 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.min_len = 1
|
||||
];
|
||||
|
||||
// The request authentication.
|
||||
//
|
||||
// Required: true
|
||||
Auth auth = 10 [(buf.validate.field).required = true];
|
||||
}
|
||||
|
||||
// This message defines attributes for a typical network response. It
|
||||
// generally models semantics of an HTTP response.
|
||||
message Response {
|
||||
// The number of items returned to the client if applicable.
|
||||
//
|
||||
// Required: false
|
||||
optional google.protobuf.Int64Value num_response_items = 1 [(buf.validate.field).int64.gte = 0];
|
||||
|
||||
// The HTTP response size in bytes.
|
||||
//
|
||||
// Required: false
|
||||
optional google.protobuf.Int64Value size = 2 [(buf.validate.field).int64.gte = 0];
|
||||
|
||||
// The HTTP response headers. If multiple headers share the same key, they
|
||||
// must be merged according to HTTP spec. All header keys must be
|
||||
// lowercased, because HTTP header keys are case-insensitive.
|
||||
//
|
||||
// Required: false
|
||||
map<string, string> headers = 3;
|
||||
|
||||
// The timestamp when the "destination" service generates the first byte of
|
||||
// the response.
|
||||
//
|
||||
// Required: true
|
||||
google.protobuf.Timestamp time = 4 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).timestamp.lt_now = true
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Metadata about the request.
|
||||
message RequestMetadata {
|
||||
// The IP address of the caller.
|
||||
// For caller from internet, this will be public IPv4 or IPv6 address.
|
||||
// For caller from a VM / K8s Service / etc, this will be the SIT proxy's IPv4 address.
|
||||
//
|
||||
// Required: true
|
||||
string caller_ip = 1 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.ip = true
|
||||
];
|
||||
|
||||
// The user agent of the caller.
|
||||
//
|
||||
// Examples:
|
||||
// "OpenAPI-Generator/1.0.0/go"
|
||||
// -> The request was made by the STACKIT SDK GO client, STACKIT CLI or Terraform provider
|
||||
// "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
||||
// -> The request was made by a web browser.
|
||||
//
|
||||
// Required: true
|
||||
string caller_supplied_user_agent = 2 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.min_len = 1,
|
||||
(buf.validate.field).string.max_len = 255
|
||||
];
|
||||
|
||||
// This field contains request attributes like request url, time, etc.
|
||||
//
|
||||
// Required: true
|
||||
AttributeContext.Request request_attributes = 3 [(buf.validate.field).required = true];
|
||||
}
|
||||
|
||||
// Metadata about the response
|
||||
message ResponseMetadata {
|
||||
// The http or gRPC status code.
|
||||
//
|
||||
// Examples:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
|
||||
// https://grpc.github.io/grpc/core/md_doc_statuscodes.html
|
||||
//
|
||||
// Required: true
|
||||
google.protobuf.Int32Value status_code = 1 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).int32.gte = 0
|
||||
];
|
||||
|
||||
// Short description of the error
|
||||
//
|
||||
// Required: false
|
||||
optional string error_message = 2;
|
||||
|
||||
// Error details
|
||||
//
|
||||
// Required: false
|
||||
repeated google.protobuf.Struct error_details = 3;
|
||||
|
||||
// This field contains response attributes like headers, time, etc.
|
||||
//
|
||||
// Required: true
|
||||
AttributeContext.Response response_attributes = 4 [(buf.validate.field).required = true];
|
||||
}
|
||||
|
||||
// Identity delegation history of an authenticated service account.
|
||||
message ServiceAccountDelegationInfo {
|
||||
// Anonymous system principal to be used when no user identity is available.
|
||||
message SystemPrincipal {
|
||||
// Metadata about the service that uses the service account.
|
||||
//
|
||||
// Required: false
|
||||
optional google.protobuf.Struct service_metadata = 1;
|
||||
}
|
||||
|
||||
// STACKIT idp principal.
|
||||
message IdpPrincipal {
|
||||
// STACKIT principal id
|
||||
//
|
||||
// Required: true
|
||||
string principal_id = 1 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.min_len = 1
|
||||
];
|
||||
|
||||
// The email address of the authenticated user.
|
||||
// Service accounts have email addresses that can be used.
|
||||
//
|
||||
// Required: true
|
||||
string principal_email = 2 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.min_len = 1,
|
||||
(buf.validate.field).string.max_len = 255
|
||||
];
|
||||
|
||||
// Metadata about the service that uses the service account.
|
||||
//
|
||||
// Required: false
|
||||
optional google.protobuf.Struct service_metadata = 3;
|
||||
}
|
||||
|
||||
// Entity that creates credentials for service account and assumes its
|
||||
// identity for authentication.
|
||||
oneof authority {
|
||||
option (buf.validate.oneof).required = true;
|
||||
|
||||
// System identity
|
||||
SystemPrincipal system_principal = 1;
|
||||
|
||||
// STACKIT IDP identity
|
||||
IdpPrincipal idp_principal = 2;
|
||||
}
|
||||
}
|
||||
135
proto/audit/v1/routable_event.proto
Normal file
135
proto/audit/v1/routable_event.proto
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package audit.v1;
|
||||
|
||||
import "buf/validate/validate.proto";
|
||||
|
||||
option go_package = "./audit;auditV1";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "com.schwarz.stackit.audit.v1";
|
||||
|
||||
enum Visibility {
|
||||
VISIBILITY_UNSPECIFIED = 0;
|
||||
// Will be routed to customer data sinks
|
||||
VISIBILITY_PUBLIC = 1;
|
||||
// Will NOT be routed to customer data sinks
|
||||
VISIBILITY_PRIVATE = 2;
|
||||
}
|
||||
|
||||
// Identifier of an object.
|
||||
//
|
||||
// For system events, the nil UUID must be used: 00000000-0000-0000-0000-000000000000.
|
||||
message ObjectIdentifier {
|
||||
// Identifier of the respective entity (e.g. Identifier of an organization)
|
||||
//
|
||||
// Required: true
|
||||
string identifier = 1 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.uuid = true
|
||||
];
|
||||
|
||||
// Entity data type relevant for routing - one of the list of supported object types.
|
||||
//
|
||||
// Required: true
|
||||
string type = 2 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.min_len = 1
|
||||
];
|
||||
}
|
||||
|
||||
message EncryptedData {
|
||||
// Encrypted serialized protobuf content (the actual audit event)
|
||||
//
|
||||
// Required: true
|
||||
bytes data = 1 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).bytes.min_len = 1
|
||||
];
|
||||
|
||||
// Name of the protobuf type
|
||||
//
|
||||
// Required: true
|
||||
string protobuf_type = 2 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.min_len = 1
|
||||
];
|
||||
|
||||
// The password taken to derive the encryption key from
|
||||
//
|
||||
// Required: true
|
||||
string encrypted_password = 3 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.min_len = 1
|
||||
];
|
||||
|
||||
// Version of the encrypted key
|
||||
//
|
||||
// Required: true
|
||||
int32 key_version = 4 [(buf.validate.field).int32.gte = 1];
|
||||
}
|
||||
|
||||
message UnencryptedData {
|
||||
// Unencrypted serialized protobuf content (the actual audit event)
|
||||
//
|
||||
// Required: true
|
||||
bytes data = 1 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).bytes.min_len = 1
|
||||
];
|
||||
|
||||
// Name of the protobuf type
|
||||
//
|
||||
// Required: true
|
||||
string protobuf_type = 2 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.min_len = 1
|
||||
];
|
||||
}
|
||||
|
||||
message RoutableAuditEvent {
|
||||
// Functional event name with pattern
|
||||
//
|
||||
// Format: stackit.<product>.<version>.<type-chain>.<operation>
|
||||
// Where:
|
||||
// Product: The name of the service in lowercase
|
||||
// Version: Optional API version
|
||||
// Type-Chain: Chained path to object
|
||||
// Operation: The name of the operation in lowercase
|
||||
//
|
||||
// Examples:
|
||||
// "stackit.resource-manager.v1.organizations.create"
|
||||
// "stackit.authorization.v1.projects.volumes.create"
|
||||
// "stackit.authorization.v2alpha.projects.volumes.create"
|
||||
// "stackit.authorization.v2.folders.move"
|
||||
// "stackit.resource-manager.health"
|
||||
//
|
||||
// Required: true
|
||||
string operation_name = 1 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.pattern = "^stackit\\.[a-z0-9-]+\\.(?:v[0-9]+\\.)?(?:[a-z0-9-.]+\\.)?[a-z0-9-]+$"
|
||||
];
|
||||
|
||||
// Visibility relevant for differentiating between internal and public events
|
||||
//
|
||||
// Required: true
|
||||
Visibility visibility = 2 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).enum.defined_only = true
|
||||
];
|
||||
|
||||
// Identifier the audit log event refers to.
|
||||
//
|
||||
// System events, will not be routed to the end-user.
|
||||
//
|
||||
// Required: true
|
||||
ObjectIdentifier object_identifier = 3 [(buf.validate.field).required = true];
|
||||
|
||||
// The actual audit event is transferred in one of the attributes below
|
||||
//
|
||||
// Required: true
|
||||
oneof data {
|
||||
option (buf.validate.oneof).required = true;
|
||||
UnencryptedData unencrypted_data = 4;
|
||||
EncryptedData encrypted_data = 5;
|
||||
}
|
||||
}
|
||||
11
proto/buf.gen.yaml
Normal file
11
proto/buf.gen.yaml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
version: v2
|
||||
plugins:
|
||||
- local: protoc-gen-go
|
||||
out: ../gen/go
|
||||
opt:
|
||||
- paths=source_relative
|
||||
- local: protoc-gen-validate
|
||||
out: ../gen/go
|
||||
opt:
|
||||
- paths=source_relative
|
||||
- lang=go
|
||||
8
proto/buf.lock
Normal file
8
proto/buf.lock
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Generated by buf. DO NOT EDIT.
|
||||
version: v1
|
||||
deps:
|
||||
- remote: buf.build
|
||||
owner: bufbuild
|
||||
repository: protovalidate
|
||||
commit: a6c49f84cc0f4e038680d390392e2ab0
|
||||
digest: shake256:3deb629c655e469d87c58babcfbed403275a741fb4a269366c4fd6ea9db012cf562a1e64819508d73670c506f96d01f724c43bc97b44e2e02aa6e8bbdd160ab2
|
||||
9
proto/buf.yaml
Normal file
9
proto/buf.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
version: v1
|
||||
breaking:
|
||||
use:
|
||||
- FILE
|
||||
deps:
|
||||
- buf.build/bufbuild/protovalidate
|
||||
lint:
|
||||
use:
|
||||
- STANDARD
|
||||
24
telemetry/telemetry.go
Normal file
24
telemetry/telemetry.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package telemetry
|
||||
|
||||
import (
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
var AuditGoVersion = GetLibVersion("dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git")
|
||||
var AuditGoGrpcVersion = GetLibVersion("dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go-grpc.git")
|
||||
var AuditGoHttpVersion = GetLibVersion("dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go-http.git")
|
||||
|
||||
func GetLibVersion(libName string) string {
|
||||
undefined := ""
|
||||
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return undefined
|
||||
}
|
||||
for _, dep := range bi.Deps {
|
||||
if dep.Path == libName {
|
||||
return dep.Version
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
Loading…
Reference in a new issue