mirror of
https://dev.azure.com/schwarzit/schwarzit.stackit-public/_git/audit-go
synced 2026-02-09 01:27:26 +00:00
Compare commits
31 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80326d88a6 | ||
|
|
b10f6797ff | ||
|
|
84c49f2690 | ||
|
|
4c7c36c8f1 | ||
|
|
a706af62a4 | ||
|
|
f2715624e9 | ||
|
|
40eacfe4ad | ||
|
|
cdea0ac81a | ||
|
|
85aae1c2e7 | ||
|
|
56b04b94cb | ||
|
|
6af0e83a95 | ||
|
|
6b5bc6dfe2 | ||
|
|
e8567c19ff | ||
|
|
618be58a26 | ||
|
|
68cec628e0 | ||
|
|
3eb803ae1c | ||
|
|
ddee3db2fe | ||
|
|
720a1a6d72 | ||
|
|
3472ce1585 | ||
|
|
5742604629 | ||
|
|
c90ce29c51 | ||
|
|
6041fd105a | ||
|
|
8b231d9a7b | ||
|
|
3eece2ddb5 | ||
|
|
5fd40d1415 | ||
|
|
bc3de128d4 | ||
|
|
cc53ba4126 | ||
|
|
568c5cdb91 | ||
|
|
7623ffc225 | ||
|
|
52592e6b6b | ||
|
|
9337231a6f |
72 changed files with 18896 additions and 0 deletions
127
.azuredevops/build-pipeline.yml
Normal file
127
.azuredevops/build-pipeline.yml
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
pool:
|
||||||
|
vmImage: 'ubuntu-24.04'
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- name: bufVersion
|
||||||
|
# go install github.com/bufbuild/buf/cmd/buf@
|
||||||
|
value: v1.63.0
|
||||||
|
- name: golangCiLintVersion
|
||||||
|
# github.com/golangci/golangci-lint
|
||||||
|
value: v2.8.0
|
||||||
|
- name: goVersion
|
||||||
|
# github.com/golang/go
|
||||||
|
value: 1.24.0
|
||||||
|
- name: protobufValidateVersion
|
||||||
|
# go install github.com/envoyproxy/protoc-gen-validate@
|
||||||
|
value: v1.3.0
|
||||||
|
- name: protobufVersion
|
||||||
|
# go install google.golang.org/protobuf/cmd/protoc-gen-go@
|
||||||
|
value: v1.36.11
|
||||||
|
- name: GOPATH
|
||||||
|
value: '$(system.defaultWorkingDirectory)/gopath'
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- stage: Build
|
||||||
|
jobs:
|
||||||
|
- job: GoBuildTest
|
||||||
|
displayName: Run build and tests
|
||||||
|
variables:
|
||||||
|
- group: artifactory-xx-sit-odj-sec-ident
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- script: echo "$(ARTIFACTORY_PASSWORD)" | docker login schwarzit-docker.jfrog.io --username $(ARTIFACTORY_USER) --password-stdin
|
||||||
|
displayName: 'Docker login'
|
||||||
|
condition: succeeded()
|
||||||
|
|
||||||
|
- 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)
|
||||||
46
.azuredevops/main-code-analyze.yml
Normal file
46
.azuredevops/main-code-analyze.yml
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
---
|
||||||
|
name: audit_go_main_code_analyze_$(Date:yyyy-MM-dd)_$(SourceBranchName)_$(Rev:r)
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
- main
|
||||||
|
|
||||||
|
resources:
|
||||||
|
repositories:
|
||||||
|
- repository: tools
|
||||||
|
type: git
|
||||||
|
name: schwarzit.stackit-core-platform/core-platform-tools
|
||||||
|
ref: refs/tags/v1.15.0
|
||||||
|
|
||||||
|
pool:
|
||||||
|
vmImage: ubuntu-24.04
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- name: reportDir
|
||||||
|
value: '$(System.DefaultWorkingDirectory)/out'
|
||||||
|
- name: goVersion
|
||||||
|
value: 1.25.5
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- stage: CodeQualityScans
|
||||||
|
displayName: "Code Quality Scans"
|
||||||
|
jobs:
|
||||||
|
- template: ./.azuredevops/templates/jobs/code/code-format.yml@tools
|
||||||
|
parameters:
|
||||||
|
lintReports: true
|
||||||
|
lintReportDir: $(reportDir)
|
||||||
|
|
||||||
|
- template: ./.azuredevops/templates/jobs/code/code-test.yml@tools
|
||||||
|
parameters:
|
||||||
|
testReports: true
|
||||||
|
testReportDir: $(reportDir)
|
||||||
|
|
||||||
|
- template: ./.azuredevops/templates/jobs/code/code-quality-scans.yml@tools
|
||||||
|
parameters:
|
||||||
|
dependsOn:
|
||||||
|
- Tests
|
||||||
|
- Linter
|
||||||
|
organization: 'xx-sit-odj-stackit-public'
|
||||||
|
serviceConnection: 'xx-sit-odj-stackit-public-snyk'
|
||||||
|
sonar: true
|
||||||
|
sonarReportSourceDir: $(reportDir)
|
||||||
|
sonarServiceConnection: sonarqube-audit-go
|
||||||
11
.azuredevops/pull_request_template.md
Normal file
11
.azuredevops/pull_request_template.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[Describe what was done in this PR]
|
||||||
|
|
||||||
|
[Describe why it was done]
|
||||||
|
|
||||||
|
[Describe how it was done if the change needs explanation]
|
||||||
|
|
||||||
|
[Describe how the change was tested if it needs explanation]
|
||||||
|
|
||||||
|
Security-concept-update-needed: false.
|
||||||
|
|
||||||
|
JIRA Work Item: [STACKITRMA-XXX](https://jira.schwarz/browse/STACKITRMA-XXX)
|
||||||
66
.azuredevops/release-pipeline.yml
Normal file
66
.azuredevops/release-pipeline.yml
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
trigger: none
|
||||||
|
|
||||||
|
pool:
|
||||||
|
vmImage: 'ubuntu-24.04'
|
||||||
|
|
||||||
|
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
|
# Editor-based Rest Client
|
||||||
.idea/httpRequests
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Buf
|
||||||
|
gen/java
|
||||||
|
gen/python
|
||||||
287
.golangci.yml
Normal file
287
.golangci.yml
Normal file
|
|
@ -0,0 +1,287 @@
|
||||||
|
version: "2"
|
||||||
|
run:
|
||||||
|
issues-exit-code: 1
|
||||||
|
tests: true
|
||||||
|
linters:
|
||||||
|
default: none
|
||||||
|
enable:
|
||||||
|
- asciicheck
|
||||||
|
- bodyclose
|
||||||
|
- copyloopvar
|
||||||
|
- cyclop
|
||||||
|
- dogsled
|
||||||
|
- dupl
|
||||||
|
- durationcheck
|
||||||
|
- errcheck
|
||||||
|
- errorlint
|
||||||
|
- exhaustive
|
||||||
|
- forbidigo
|
||||||
|
- forcetypeassert
|
||||||
|
- gochecknoglobals
|
||||||
|
- gochecknoinits
|
||||||
|
- gocognit
|
||||||
|
- goconst
|
||||||
|
- gocritic
|
||||||
|
- gocyclo
|
||||||
|
- gomoddirectives
|
||||||
|
- gomodguard
|
||||||
|
- goprintffuncname
|
||||||
|
- gosec
|
||||||
|
- govet
|
||||||
|
- importas
|
||||||
|
- ineffassign
|
||||||
|
- lll
|
||||||
|
- makezero
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
- nestif
|
||||||
|
- nilerr
|
||||||
|
- noctx
|
||||||
|
- nolintlint
|
||||||
|
- prealloc
|
||||||
|
- predeclared
|
||||||
|
- promlinter
|
||||||
|
- revive
|
||||||
|
- rowserrcheck
|
||||||
|
- sqlclosecheck
|
||||||
|
- staticcheck
|
||||||
|
- tparallel
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- unused
|
||||||
|
- wastedassign
|
||||||
|
settings:
|
||||||
|
cyclop:
|
||||||
|
max-complexity: 45
|
||||||
|
package-average: 0
|
||||||
|
dogsled:
|
||||||
|
max-blank-identifiers: 2
|
||||||
|
dupl:
|
||||||
|
threshold: 150
|
||||||
|
errcheck:
|
||||||
|
check-type-assertions: true
|
||||||
|
check-blank: true
|
||||||
|
exhaustive:
|
||||||
|
default-signifies-exhaustive: true
|
||||||
|
funlen:
|
||||||
|
lines: 100
|
||||||
|
statements: 50
|
||||||
|
gocognit:
|
||||||
|
min-complexity: 45
|
||||||
|
goconst:
|
||||||
|
min-len: 3
|
||||||
|
min-occurrences: 5
|
||||||
|
gocritic:
|
||||||
|
disabled-checks:
|
||||||
|
- dupImport
|
||||||
|
- octalLiteral
|
||||||
|
- unnamedResult
|
||||||
|
enabled-tags:
|
||||||
|
- diagnostic
|
||||||
|
- experimental
|
||||||
|
- opinionated
|
||||||
|
- performance
|
||||||
|
- style
|
||||||
|
settings:
|
||||||
|
hugeParam:
|
||||||
|
sizeThreshold: 121
|
||||||
|
gocyclo:
|
||||||
|
min-complexity: 45
|
||||||
|
govet:
|
||||||
|
disable:
|
||||||
|
- fieldalignment
|
||||||
|
enable-all: true
|
||||||
|
lll:
|
||||||
|
line-length: 180
|
||||||
|
tab-width: 1
|
||||||
|
nakedret:
|
||||||
|
max-func-lines: 5
|
||||||
|
nestif:
|
||||||
|
min-complexity: 10
|
||||||
|
nlreturn:
|
||||||
|
block-size: 5
|
||||||
|
nolintlint:
|
||||||
|
require-explanation: true
|
||||||
|
require-specific: true
|
||||||
|
allow-unused: false
|
||||||
|
prealloc:
|
||||||
|
simple: true
|
||||||
|
range-loops: true
|
||||||
|
for-loops: true
|
||||||
|
revive:
|
||||||
|
rules:
|
||||||
|
- name: context-keys-type
|
||||||
|
disabled: false
|
||||||
|
- name: time-naming
|
||||||
|
disabled: false
|
||||||
|
- name: var-declaration
|
||||||
|
disabled: false
|
||||||
|
- name: unexported-return
|
||||||
|
disabled: false
|
||||||
|
- name: errorf
|
||||||
|
disabled: false
|
||||||
|
- name: blank-imports
|
||||||
|
disabled: false
|
||||||
|
- name: context-as-argument
|
||||||
|
disabled: false
|
||||||
|
- name: dot-imports
|
||||||
|
disabled: false
|
||||||
|
- name: error-return
|
||||||
|
disabled: false
|
||||||
|
- name: error-strings
|
||||||
|
disabled: false
|
||||||
|
- name: error-naming
|
||||||
|
disabled: false
|
||||||
|
- name: exported
|
||||||
|
disabled: false
|
||||||
|
- name: increment-decrement
|
||||||
|
disabled: false
|
||||||
|
- name: var-naming
|
||||||
|
disabled: true
|
||||||
|
- name: package-comments
|
||||||
|
disabled: false
|
||||||
|
- name: range
|
||||||
|
disabled: false
|
||||||
|
- name: receiver-naming
|
||||||
|
disabled: false
|
||||||
|
- name: indent-error-flow
|
||||||
|
disabled: false
|
||||||
|
staticcheck:
|
||||||
|
initialisms:
|
||||||
|
- ACL
|
||||||
|
- API
|
||||||
|
- ASCII
|
||||||
|
- CPU
|
||||||
|
- CSS
|
||||||
|
- DNS
|
||||||
|
- EOF
|
||||||
|
- GUID
|
||||||
|
- HTML
|
||||||
|
- HTTP
|
||||||
|
- HTTPS
|
||||||
|
- ID
|
||||||
|
- IP
|
||||||
|
- JSON
|
||||||
|
- QPS
|
||||||
|
- RAM
|
||||||
|
- RPC
|
||||||
|
- SLA
|
||||||
|
- SMTP
|
||||||
|
- SQL
|
||||||
|
- SSH
|
||||||
|
- TCP
|
||||||
|
- TLS
|
||||||
|
- TTL
|
||||||
|
- UDP
|
||||||
|
- UI
|
||||||
|
- GID
|
||||||
|
- UID
|
||||||
|
- UUID
|
||||||
|
- URI
|
||||||
|
- URL
|
||||||
|
- UTF8
|
||||||
|
- VM
|
||||||
|
- XML
|
||||||
|
- XMPP
|
||||||
|
- XSRF
|
||||||
|
- XSS
|
||||||
|
- SIP
|
||||||
|
- RTP
|
||||||
|
- AMQP
|
||||||
|
- DB
|
||||||
|
- TS
|
||||||
|
unparam:
|
||||||
|
check-exported: false
|
||||||
|
unused:
|
||||||
|
exported-fields-are-used: false
|
||||||
|
whitespace:
|
||||||
|
multi-if: false
|
||||||
|
multi-func: false
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
presets:
|
||||||
|
- comments
|
||||||
|
- common-false-positives
|
||||||
|
- legacy
|
||||||
|
- std-error-handling
|
||||||
|
rules:
|
||||||
|
- path: internal/audit/api/api_common.go
|
||||||
|
text: context-as-argument
|
||||||
|
- linters:
|
||||||
|
- gochecknoglobals
|
||||||
|
path: pkg/audit/common/api.go|pkg/log/log.go|internal/telemetry/telemetry.go
|
||||||
|
- linters:
|
||||||
|
- dupl
|
||||||
|
path: pkg/audit/api/api_.*.go
|
||||||
|
- path: internal/audit/api/model_test.go|internal/audit/api/model.go
|
||||||
|
text: G115
|
||||||
|
- linters:
|
||||||
|
- gosec
|
||||||
|
path: internal/audit/api/test_data.go
|
||||||
|
- linters:
|
||||||
|
- dogsled
|
||||||
|
- dupl
|
||||||
|
- errcheck
|
||||||
|
- forbidigo
|
||||||
|
- forcetypeassert
|
||||||
|
- gochecknoglobals
|
||||||
|
- gocognit
|
||||||
|
- goconst
|
||||||
|
- gocritic
|
||||||
|
- ineffassign
|
||||||
|
- lll
|
||||||
|
- nakedret
|
||||||
|
- nestif
|
||||||
|
- nlreturn
|
||||||
|
- noctx
|
||||||
|
- revive
|
||||||
|
- staticcheck
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- wastedassign
|
||||||
|
- wsl
|
||||||
|
path: _test\.go
|
||||||
|
- linters:
|
||||||
|
- govet
|
||||||
|
text: declaration of "err" shadows declaration
|
||||||
|
- linters:
|
||||||
|
- dogsled
|
||||||
|
- dupl
|
||||||
|
- errcheck
|
||||||
|
- forbidigo
|
||||||
|
- forcetypeassert
|
||||||
|
- gochecknoglobals
|
||||||
|
- gocognit
|
||||||
|
- goconst
|
||||||
|
- gocritic
|
||||||
|
- ineffassign
|
||||||
|
- lll
|
||||||
|
- nakedret
|
||||||
|
- nestif
|
||||||
|
- nlreturn
|
||||||
|
- noctx
|
||||||
|
- revive
|
||||||
|
- staticcheck
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- wastedassign
|
||||||
|
- wsl
|
||||||
|
path: test_.*\.go|pkg/messaging/test/solace.go
|
||||||
|
- linters:
|
||||||
|
- prealloc
|
||||||
|
path: internal/messaging/amqp_connection_pool_test.go
|
||||||
|
text: Consider preallocating connections with capacity 5
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
issues:
|
||||||
|
max-issues-per-linter: 0
|
||||||
|
max-same-issues: 0
|
||||||
|
formatters:
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
82
Makefile
Normal file
82
Makefile
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
SHELL = /bin/bash -euo pipefail
|
||||||
|
PWD = $(shell pwd)
|
||||||
|
export PATH := $(PWD)/bin:$(PATH)
|
||||||
|
|
||||||
|
# constants
|
||||||
|
GOLANGCI_VERSION = 2.8.0
|
||||||
|
|
||||||
|
all: download build ## Initializes all tools and files
|
||||||
|
all/ci: ado-git-setup all
|
||||||
|
|
||||||
|
out:
|
||||||
|
@mkdir -pv "$(@)"
|
||||||
|
|
||||||
|
build: out ## do nothing
|
||||||
|
|
||||||
|
.PHONY: build/%
|
||||||
|
build/%: out ## do nothing
|
||||||
|
|
||||||
|
download:
|
||||||
|
@go mod download
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
@go fmt ./...
|
||||||
|
|
||||||
|
GOLANGCI_LINT = bin/golangci-lint-$(GOLANGCI_VERSION)
|
||||||
|
$(GOLANGCI_LINT):
|
||||||
|
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | bash -s -- -b bin v$(GOLANGCI_VERSION)
|
||||||
|
@mv bin/golangci-lint "$(@)"
|
||||||
|
|
||||||
|
lint: fmt $(GOLANGCI_LINT) download ## Lints all code with golangci-lint
|
||||||
|
@$(GOLANGCI_LINT) run
|
||||||
|
|
||||||
|
lint/fix: fmt $(GOLANGCI_LINT) download ## Fixes automatically fixable things like imports for the defined lint rules
|
||||||
|
@$(GOLANGCI_LINT) run --fix
|
||||||
|
|
||||||
|
lint/reports: fmt $(GOLANGCI_LINT) download ## Fixes automatically fixable things like imports for the defined lint rules
|
||||||
|
@$(GOLANGCI_LINT) run ./... --output.checkstyle.path stdout | awk '!/0 issues./' > out/lint.xml
|
||||||
|
|
||||||
|
test-clean:
|
||||||
|
@go clean -testcache
|
||||||
|
|
||||||
|
tidy:
|
||||||
|
@go mod tidy
|
||||||
|
|
||||||
|
test:
|
||||||
|
@go test ./...
|
||||||
|
|
||||||
|
coverage: out/report.json ## Displays coverage per func on cli
|
||||||
|
go tool cover -func=out/cover.out
|
||||||
|
|
||||||
|
html-coverage: out/report.json ## Displays the coverage results in the browser
|
||||||
|
go tool cover -html=out/cover.out
|
||||||
|
|
||||||
|
test-reports: out/report.json
|
||||||
|
|
||||||
|
.PHONY: out/report.json
|
||||||
|
out/report.json: out
|
||||||
|
go test -v $$(go list ./... | grep -v '/tests') -tags=unit -coverprofile=out/cover.out -json | tee "$(@)"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@rm -rf bin out
|
||||||
|
|
||||||
|
.PHONY: ado-git-setup
|
||||||
|
ado-git-setup:
|
||||||
|
# Add "dev.azure.com/schwarzit" to GOPRIVATE if not present
|
||||||
|
@priv="$$(go env GOPRIVATE)"; \
|
||||||
|
[[ "$$priv" =~ '(^|,)dev\.azure\.com(/|,|$)' ]] || go env -w "GOPRIVATE=$${priv:+$$priv,}dev.azure.com/schwarzit"
|
||||||
|
# Configure HTTPS (with PAT) or SSH access to Go import paths
|
||||||
|
@if [[ -n "$${ADO_PAT:+x}" ]]; then \
|
||||||
|
git config --global "url.https://schwarzit:$${ADO_PAT}@dev.azure.com/schwarzit/.insteadof" 'https://dev.azure.com/schwarzit/'; \
|
||||||
|
else \
|
||||||
|
git config --global 'url.git@ssh.dev.azure.com:v3/schwarzit.insteadOf' 'https://dev.azure.com/schwarzit'; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo 'Usage: make <OPTIONS> ... <TARGETS>'
|
||||||
|
@echo ''
|
||||||
|
@echo 'Available targets are:'
|
||||||
|
@echo ''
|
||||||
|
@grep -E '^[ a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
||||||
|
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||||
|
@echo ''
|
||||||
129
README.md
Normal file
129
README.md
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
> ## DEPRECATION NOTICE
|
||||||
|
>
|
||||||
|
> ### Discontinuation of the current audit log system
|
||||||
|
>
|
||||||
|
> The audit log system provided to date will be discontinued in its current form.
|
||||||
|
> This decision was made to pave the way for a new, more powerful audit log system that
|
||||||
|
> will be provided in the future. The new system offers extended functionalities and
|
||||||
|
> improved integration options, particularly with regard to the use and analysis of
|
||||||
|
> audit data by our customers.
|
||||||
|
>
|
||||||
|
> ### What does it mean?
|
||||||
|
> The existing audit log system will be supported until the new system is generally
|
||||||
|
> available to customers on Mai 1, 2026.
|
||||||
|
> **Services that are already sending audit log events to the existing audit log
|
||||||
|
> system must continue to do so until the new system is GA** and further information
|
||||||
|
> about the shutdown process is provided.
|
||||||
|
> **Large volumes of new audit event types must not be sent to the existing audit log
|
||||||
|
> system.**
|
||||||
|
>
|
||||||
|
> STACKIT services should start migrating to the new system now by sending data to
|
||||||
|
> the new system (**in parallel**).
|
||||||
|
> **The new audit log system may drop and does not guarantee to store events until
|
||||||
|
> it will be GA**.
|
||||||
|
> Further information on the changeover and how to use the new system can be found in the
|
||||||
|
> [developer docs](https://developers.stackit.schwarz/domains/central-services/telemetry-router/integration/).
|
||||||
|
>
|
||||||
|
> We are confident that the new audit log system will make an important contribution to
|
||||||
|
> improving the transparency, traceability, and integration for our customers.
|
||||||
|
> If you have any questions or need assistance, the
|
||||||
|
> [STACKIT Telemetry Hub](https://chat.google.com/room/AAQAf9NsX6M?cls=7) team will be
|
||||||
|
> happy to help.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
#### Go
|
||||||
|
The current minimum Go version is **go1.24.0**.
|
||||||
|
|
||||||
|
#### 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 v2.8.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.63.0 #Pipeline: bufVersion
|
||||||
|
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11 #Pipeline: protobufVersion, go.mod: buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go
|
||||||
|
go install github.com/envoyproxy/protoc-gen-validate@v1.3.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>
|
||||||
2
buf.lock
Normal file
2
buf.lock
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Generated by buf. DO NOT EDIT.
|
||||||
|
version: v1
|
||||||
1717
gen/go/audit/v1/audit_event.pb.go
Normal file
1717
gen/go/audit/v1/audit_event.pb.go
Normal file
File diff suppressed because it is too large
Load diff
2203
gen/go/audit/v1/audit_event.pb.validate.go
Normal file
2203
gen/go/audit/v1/audit_event.pb.validate.go
Normal file
File diff suppressed because it is too large
Load diff
513
gen/go/audit/v1/routable_event.pb.go
Normal file
513
gen/go/audit/v1/routable_event.pb.go
Normal file
|
|
@ -0,0 +1,513 @@
|
||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.36.11
|
||||||
|
// 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"
|
||||||
|
unsafe "unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 `protogen:"open.v1"`
|
||||||
|
// 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"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
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 `protogen:"open.v1"`
|
||||||
|
// 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"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
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 `protogen:"open.v1"`
|
||||||
|
// 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"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
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 `protogen:"open.v1"`
|
||||||
|
// 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 valid to be assigned to Data:
|
||||||
|
//
|
||||||
|
// *RoutableAuditEvent_UnencryptedData
|
||||||
|
// *RoutableAuditEvent_EncryptedData
|
||||||
|
Data isRoutableAuditEvent_Data `protobuf_oneof:"data"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (x *RoutableAuditEvent) GetData() isRoutableAuditEvent_Data {
|
||||||
|
if x != nil {
|
||||||
|
return x.Data
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RoutableAuditEvent) GetUnencryptedData() *UnencryptedData {
|
||||||
|
if x != nil {
|
||||||
|
if x, ok := x.Data.(*RoutableAuditEvent_UnencryptedData); ok {
|
||||||
|
return x.UnencryptedData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RoutableAuditEvent) GetEncryptedData() *EncryptedData {
|
||||||
|
if x != nil {
|
||||||
|
if x, ok := x.Data.(*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
|
||||||
|
|
||||||
|
const file_audit_v1_routable_event_proto_rawDesc = "" +
|
||||||
|
"\n" +
|
||||||
|
"\x1daudit/v1/routable_event.proto\x12\baudit.v1\x1a\x1bbuf/validate/validate.proto\"_\n" +
|
||||||
|
"\x10ObjectIdentifier\x12+\n" +
|
||||||
|
"\n" +
|
||||||
|
"identifier\x18\x01 \x01(\tB\v\xbaH\b\xc8\x01\x01r\x03\xb0\x01\x01R\n" +
|
||||||
|
"identifier\x12\x1e\n" +
|
||||||
|
"\x04type\x18\x02 \x01(\tB\n" +
|
||||||
|
"\xbaH\a\xc8\x01\x01r\x02\x10\x01R\x04type\"\xc5\x01\n" +
|
||||||
|
"\rEncryptedData\x12\x1e\n" +
|
||||||
|
"\x04data\x18\x01 \x01(\fB\n" +
|
||||||
|
"\xbaH\a\xc8\x01\x01z\x02\x10\x01R\x04data\x12/\n" +
|
||||||
|
"\rprotobuf_type\x18\x02 \x01(\tB\n" +
|
||||||
|
"\xbaH\a\xc8\x01\x01r\x02\x10\x01R\fprotobufType\x129\n" +
|
||||||
|
"\x12encrypted_password\x18\x03 \x01(\tB\n" +
|
||||||
|
"\xbaH\a\xc8\x01\x01r\x02\x10\x01R\x11encryptedPassword\x12(\n" +
|
||||||
|
"\vkey_version\x18\x04 \x01(\x05B\a\xbaH\x04\x1a\x02(\x01R\n" +
|
||||||
|
"keyVersion\"b\n" +
|
||||||
|
"\x0fUnencryptedData\x12\x1e\n" +
|
||||||
|
"\x04data\x18\x01 \x01(\fB\n" +
|
||||||
|
"\xbaH\a\xc8\x01\x01z\x02\x10\x01R\x04data\x12/\n" +
|
||||||
|
"\rprotobuf_type\x18\x02 \x01(\tB\n" +
|
||||||
|
"\xbaH\a\xc8\x01\x01r\x02\x10\x01R\fprotobufType\"\xb5\x03\n" +
|
||||||
|
"\x12RoutableAuditEvent\x12r\n" +
|
||||||
|
"\x0eoperation_name\x18\x01 \x01(\tBK\xbaHH\xc8\x01\x01rC2A^stackit\\.[a-z0-9-]+\\.(?:v[0-9]+\\.)?(?:[a-z0-9-.]+\\.)?[a-z0-9-]+$R\roperationName\x12A\n" +
|
||||||
|
"\n" +
|
||||||
|
"visibility\x18\x02 \x01(\x0e2\x14.audit.v1.VisibilityB\v\xbaH\b\xc8\x01\x01\x82\x01\x02\x10\x01R\n" +
|
||||||
|
"visibility\x12O\n" +
|
||||||
|
"\x11object_identifier\x18\x03 \x01(\v2\x1a.audit.v1.ObjectIdentifierB\x06\xbaH\x03\xc8\x01\x01R\x10objectIdentifier\x12F\n" +
|
||||||
|
"\x10unencrypted_data\x18\x04 \x01(\v2\x19.audit.v1.UnencryptedDataH\x00R\x0funencryptedData\x12@\n" +
|
||||||
|
"\x0eencrypted_data\x18\x05 \x01(\v2\x17.audit.v1.EncryptedDataH\x00R\rencryptedDataB\r\n" +
|
||||||
|
"\x04data\x12\x05\xbaH\x02\b\x01*W\n" +
|
||||||
|
"\n" +
|
||||||
|
"Visibility\x12\x1a\n" +
|
||||||
|
"\x16VISIBILITY_UNSPECIFIED\x10\x00\x12\x15\n" +
|
||||||
|
"\x11VISIBILITY_PUBLIC\x10\x01\x12\x16\n" +
|
||||||
|
"\x12VISIBILITY_PRIVATE\x10\x02B1\n" +
|
||||||
|
"\x1ccom.schwarz.stackit.audit.v1P\x01Z\x0f./audit;auditV1b\x06proto3"
|
||||||
|
|
||||||
|
var (
|
||||||
|
file_audit_v1_routable_event_proto_rawDescOnce sync.Once
|
||||||
|
file_audit_v1_routable_event_proto_rawDescData []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
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(unsafe.Slice(unsafe.StringData(file_audit_v1_routable_event_proto_rawDesc), len(file_audit_v1_routable_event_proto_rawDesc)))
|
||||||
|
})
|
||||||
|
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: unsafe.Slice(unsafe.StringData(file_audit_v1_routable_event_proto_rawDesc), len(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_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 {
|
||||||
|
msgs := make([]string, 0, len(m))
|
||||||
|
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 {
|
||||||
|
msgs := make([]string, 0, len(m))
|
||||||
|
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 {
|
||||||
|
msgs := make([]string, 0, len(m))
|
||||||
|
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 {
|
||||||
|
msgs := make([]string, 0, len(m))
|
||||||
|
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{}
|
||||||
88
go.mod
Normal file
88
go.mod
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
module dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git
|
||||||
|
|
||||||
|
go 1.24.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1
|
||||||
|
buf.build/go/protovalidate v1.1.0
|
||||||
|
github.com/Azure/go-amqp v1.5.1
|
||||||
|
github.com/docker/docker v28.5.2+incompatible
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/lestrrat-go/jwx/v2 v2.1.6
|
||||||
|
github.com/rs/zerolog v1.34.0
|
||||||
|
github.com/stretchr/testify v1.11.1
|
||||||
|
github.com/testcontainers/testcontainers-go v0.40.0
|
||||||
|
go.opentelemetry.io/otel v1.39.0
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0
|
||||||
|
google.golang.org/protobuf v1.36.11
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
cel.dev/expr v0.25.1 // indirect
|
||||||
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 // 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/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||||
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
github.com/docker/go-connections v0.6.0 // indirect
|
||||||
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
|
github.com/ebitengine/purego v0.9.1 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/google/cel-go v0.26.1 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.2 // indirect
|
||||||
|
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
||||||
|
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||||
|
github.com/lestrrat-go/httprc v1.0.6 // indirect
|
||||||
|
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||||
|
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||||
|
github.com/magiconair/properties v1.8.10 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
|
github.com/moby/go-archive v0.2.0 // indirect
|
||||||
|
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||||
|
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||||
|
github.com/moby/sys/user v0.4.0 // indirect
|
||||||
|
github.com/moby/sys/userns v0.1.0 // indirect
|
||||||
|
github.com/moby/term v0.5.2 // indirect
|
||||||
|
github.com/morikuni/aec v1.1.0 // indirect
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 // 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-20240221224432-82ca36839d55 // indirect
|
||||||
|
github.com/segmentio/asm v1.2.1 // indirect
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.12 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
|
github.com/stoewer/go-strcase v1.3.1 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.3 // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||||
|
golang.org/x/crypto v0.46.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
|
||||||
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
|
golang.org/x/text v0.33.0 // indirect
|
||||||
|
golang.org/x/time v0.14.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
227
go.sum
Normal file
227
go.sum
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 h1:j9yeqTWEFrtimt8Nng2MIeRrpoCvQzM9/g25XTvqUGg=
|
||||||
|
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
|
||||||
|
buf.build/go/protovalidate v1.1.0 h1:pQqEQRpOo4SqS60qkvmhLTTQU9JwzEvdyiqAtXa5SeY=
|
||||||
|
buf.build/go/protovalidate v1.1.0/go.mod h1:bGZcPiAQDC3ErCHK3t74jSoJDFOs2JH3d7LWuTEIdss=
|
||||||
|
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||||
|
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||||
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||||
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||||
|
github.com/Azure/go-amqp v1.5.1 h1:WyiPTz2C3zVvDL7RLAqwWdeoYhMtX62MZzQoP09fzsU=
|
||||||
|
github.com/Azure/go-amqp v1.5.1/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/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.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||||
|
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||||
|
github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
|
||||||
|
github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
|
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/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||||
|
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 v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||||
|
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||||
|
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||||
|
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/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||||
|
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
|
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.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/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/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
|
||||||
|
github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
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.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||||
|
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
|
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
|
||||||
|
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
|
||||||
|
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||||
|
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||||
|
github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k=
|
||||||
|
github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||||
|
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||||
|
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||||
|
github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA=
|
||||||
|
github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU=
|
||||||
|
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||||
|
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
|
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||||
|
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/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/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
|
||||||
|
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
|
||||||
|
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/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||||
|
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||||
|
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||||
|
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||||
|
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||||
|
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||||
|
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||||
|
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||||
|
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||||
|
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
|
||||||
|
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
|
||||||
|
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.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||||
|
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-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
|
github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8=
|
||||||
|
github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
|
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||||
|
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
|
||||||
|
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.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
|
||||||
|
github.com/stoewer/go-strcase v1.3.1/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.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||||
|
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
|
||||||
|
github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||||
|
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||||
|
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||||
|
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU=
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||||
|
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||||
|
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||||
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||||
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||||
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||||
|
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||||
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
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.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
233
internal/audit/api/api_common.go
Normal file
233
internal/audit/api/api_common.go
Normal file
|
|
@ -0,0 +1,233 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
internalTelemetry "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/internal/telemetry"
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
pkgMessagingApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
var TopicNamePattern = regexp.MustCompile(`^topic://stackit-platform/t/swz/audit-log/(?:conway|eu01|eu02|sx-stoi01)/[Vv][1-9](?:\.\d)?/[A-Za-z0-9-]+/[A-Za-z0-9-/]+`)
|
||||||
|
|
||||||
|
func ValidateAndSerializePartially(
|
||||||
|
validator pkgAuditCommon.ProtobufValidator,
|
||||||
|
event *auditV1.AuditLogEntry,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
routableIdentifier *pkgAuditCommon.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 pkgAuditCommon.ProtobufValidator,
|
||||||
|
event *auditV1.AuditLogEntry,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
routableIdentifier *pkgAuditCommon.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 pkgAuditCommon.ProtobufValidator,
|
||||||
|
event *auditV1.AuditLogEntry,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
routableIdentifier *pkgAuditCommon.RoutableIdentifier,
|
||||||
|
) error {
|
||||||
|
|
||||||
|
// Return error if the given event or object identifier is nil
|
||||||
|
if event == nil {
|
||||||
|
return pkgAuditCommon.ErrEventNil
|
||||||
|
}
|
||||||
|
if routableIdentifier == nil {
|
||||||
|
return pkgAuditCommon.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 pkgAuditCommon.ErrObjectIdentifierVisibilityMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that provided identifier type is supported
|
||||||
|
if err := routableIdentifier.Type.IsSupportedType(); err != nil {
|
||||||
|
if errors.Is(err, pkgAuditCommon.ErrUnknownObjectType) {
|
||||||
|
return pkgAuditCommon.ErrUnsupportedRoutableType
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check identifier consistency across event attributes
|
||||||
|
if strings.HasSuffix(event.LogName, string(pkgAuditCommon.EventTypeSystemEvent)) {
|
||||||
|
if routableIdentifier.Identifier != pkgAuditCommon.SystemIdentifier.Identifier || routableIdentifier.Type != pkgAuditCommon.ObjectTypeSystem {
|
||||||
|
return pkgAuditCommon.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 pkgAuditCommon.TopicNameResolver,
|
||||||
|
messagingApi pkgMessagingApi.Api,
|
||||||
|
ctx context.Context,
|
||||||
|
routableIdentifier *pkgAuditCommon.RoutableIdentifier,
|
||||||
|
cloudEvent *pkgAuditCommon.CloudEvent,
|
||||||
|
) error {
|
||||||
|
|
||||||
|
// Check that given objects are not nil
|
||||||
|
if topicNameResolver == nil {
|
||||||
|
return pkgAuditCommon.ErrTopicNameResolverNil
|
||||||
|
}
|
||||||
|
if messagingApi == nil {
|
||||||
|
return pkgAuditCommon.ErrMessagingApiNil
|
||||||
|
}
|
||||||
|
if cloudEvent == nil {
|
||||||
|
return pkgAuditCommon.ErrCloudEventNil
|
||||||
|
}
|
||||||
|
if routableIdentifier == nil {
|
||||||
|
return pkgAuditCommon.ErrObjectIdentifierNil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that provided identifier type is supported
|
||||||
|
if err := routableIdentifier.Type.IsSupportedType(); err != nil {
|
||||||
|
if errors.Is(err, pkgAuditCommon.ErrUnknownObjectType) {
|
||||||
|
return pkgAuditCommon.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 := internalTelemetry.AuditGoVersion
|
||||||
|
if auditGoVersion != "" {
|
||||||
|
applicationAttributes["cloudEvents:sdkversion"] = auditGoVersion
|
||||||
|
}
|
||||||
|
auditGoGrpcVersion := internalTelemetry.AuditGoGrpcVersion
|
||||||
|
if auditGoGrpcVersion != "" {
|
||||||
|
applicationAttributes["cloudEvents:sdkgrpcversion"] = auditGoGrpcVersion
|
||||||
|
}
|
||||||
|
auditGoHttpVersion := internalTelemetry.AuditGoHttpVersion
|
||||||
|
if auditGoHttpVersion != "" {
|
||||||
|
applicationAttributes["cloudEvents:sdkhttpversion"] = auditGoHttpVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
return messagingApi.Send(
|
||||||
|
ctx,
|
||||||
|
topic,
|
||||||
|
cloudEvent.Data,
|
||||||
|
cloudEvent.DataContentType,
|
||||||
|
applicationAttributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSystemIdentifier(identifier *pkgAuditCommon.RoutableIdentifier) bool {
|
||||||
|
if identifier.Identifier == uuid.Nil.String() && identifier.Type == pkgAuditCommon.ObjectTypeSystem {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func areIdentifiersIdentical(routableIdentifier *pkgAuditCommon.RoutableIdentifier, logName string) error {
|
||||||
|
dataType, identifier := getTypeAndIdentifierFromString(logName)
|
||||||
|
objectType := pkgAuditCommon.ObjectTypeFromPluralString(dataType)
|
||||||
|
err := objectType.IsSupportedType()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return areTypeAndIdentifierIdentical(routableIdentifier, objectType, identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func areTypeAndIdentifierIdentical(routableIdentifier *pkgAuditCommon.RoutableIdentifier, dataType pkgAuditCommon.ObjectType, identifier string) error {
|
||||||
|
if routableIdentifier.Identifier != identifier {
|
||||||
|
return pkgAuditCommon.ErrAttributeIdentifierInvalid
|
||||||
|
}
|
||||||
|
if routableIdentifier.Type != dataType {
|
||||||
|
return pkgAuditCommon.ErrAttributeTypeInvalid
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTypeAndIdentifierFromString(input string) (string, string) {
|
||||||
|
parts := strings.Split(input, "/")
|
||||||
|
dataType := parts[0]
|
||||||
|
identifier := parts[1]
|
||||||
|
return dataType, identifier
|
||||||
|
}
|
||||||
477
internal/audit/api/api_common_test.go
Normal file
477
internal/audit/api/api_common_test.go
Normal file
|
|
@ -0,0 +1,477 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"buf.build/go/protovalidate"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
pkgMessagingApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MessagingApiMock) Close(_ context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type TopicNameResolverMock struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TopicNameResolverMock) Resolve(routableIdentifier *pkgAuditCommon.RoutableIdentifier) (string, error) {
|
||||||
|
args := m.Called(routableIdentifier)
|
||||||
|
return args.String(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewValidator(t *testing.T) pkgAuditCommon.ProtobufValidator {
|
||||||
|
validator, err := protovalidate.New()
|
||||||
|
var protoValidator pkgAuditCommon.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, pkgAuditCommon.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, pkgAuditCommon.NewRoutableIdentifier(objectIdentifier))
|
||||||
|
|
||||||
|
assert.EqualError(t, err, "validation error: log_name: value is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ValidateAndSerializePartially_RoutableEventValidationFailed(t *testing.T) {
|
||||||
|
validator := NewValidator(t)
|
||||||
|
|
||||||
|
event, objectIdentifier := NewOrganizationAuditEvent(nil)
|
||||||
|
_, err := ValidateAndSerializePartially(validator, event, 3, pkgAuditCommon.NewRoutableIdentifier(objectIdentifier))
|
||||||
|
|
||||||
|
assert.EqualError(t, err, "validation error: visibility: value must be one of the defined enum values")
|
||||||
|
}
|
||||||
|
|
||||||
|
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, pkgAuditCommon.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, pkgAuditCommon.ErrObjectIdentifierNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Visibility public - object identifier system", func(t *testing.T) {
|
||||||
|
_, err := ValidateAndSerializePartially(
|
||||||
|
validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.RoutableSystemIdentifier)
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.ErrObjectIdentifierVisibilityMismatch)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Visibility public - object identifier set", func(t *testing.T) {
|
||||||
|
routableEvent, err := ValidateAndSerializePartially(
|
||||||
|
validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.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, pkgAuditCommon.RoutableSystemIdentifier)
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.ErrAttributeIdentifierInvalid)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Visibility private - object identifier set", func(t *testing.T) {
|
||||||
|
routableEvent, err := ValidateAndSerializePartially(
|
||||||
|
validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, pkgAuditCommon.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, pkgAuditCommon.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, pkgAuditCommon.ErrObjectIdentifierNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Visibility public - object identifier system", func(t *testing.T) {
|
||||||
|
_, err := ValidateAndSerializePartially(
|
||||||
|
validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.RoutableSystemIdentifier)
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.ErrObjectIdentifierVisibilityMismatch)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Visibility public - object identifier set", func(t *testing.T) {
|
||||||
|
_, err := ValidateAndSerializePartially(
|
||||||
|
validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.NewRoutableIdentifier(
|
||||||
|
&auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: string(pkgAuditCommon.ObjectTypeOrganization)}))
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.ErrInvalidRoutableIdentifierForSystemEvent)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Visibility private - object identifier system", func(t *testing.T) {
|
||||||
|
routableEvent, err := ValidateAndSerializePartially(
|
||||||
|
validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, pkgAuditCommon.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, pkgAuditCommon.NewRoutableIdentifier(
|
||||||
|
&auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: string(pkgAuditCommon.ObjectTypeOrganization)}))
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.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, pkgAuditCommon.NewRoutableIdentifier(objectIdentifier))
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.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, pkgAuditCommon.NewRoutableIdentifier(objectIdentifier))
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.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, pkgAuditCommon.NewRoutableIdentifier(objectIdentifier))
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.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, pkgAuditCommon.NewRoutableIdentifier(objectIdentifier))
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.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, pkgAuditCommon.NewRoutableIdentifier(objectIdentifier))
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.ErrAttributeIdentifierInvalid)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ValidateAndSerializePartially_SystemEvent(t *testing.T) {
|
||||||
|
validator := NewValidator(t)
|
||||||
|
|
||||||
|
event := NewSystemAuditEvent(nil)
|
||||||
|
|
||||||
|
routableEvent, err := ValidateAndSerializePartially(
|
||||||
|
validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, pkgAuditCommon.RoutableSystemIdentifier)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, event.LogName, fmt.Sprintf("system/%s/logs/%s", pkgAuditCommon.SystemIdentifier.Identifier, pkgAuditCommon.EventTypeSystemEvent))
|
||||||
|
assert.True(t, proto.Equal(routableEvent.ObjectIdentifier, pkgAuditCommon.SystemIdentifier))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Send_TopicNameResolverNil(t *testing.T) {
|
||||||
|
err := Send(nil, nil, context.Background(), nil, nil)
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.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 pkgAuditCommon.TopicNameResolver = &topicNameResolverMock
|
||||||
|
|
||||||
|
var cloudEvent = pkgAuditCommon.CloudEvent{}
|
||||||
|
|
||||||
|
var messagingApi pkgMessagingApi.Api = &pkgMessagingApi.AmqpApi{}
|
||||||
|
err := Send(topicNameResolver, messagingApi, context.Background(), pkgAuditCommon.RoutableSystemIdentifier, &cloudEvent)
|
||||||
|
assert.ErrorIs(t, err, expectedError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Send_MessagingApiNil(t *testing.T) {
|
||||||
|
var topicNameResolver pkgAuditCommon.TopicNameResolver = &pkgAuditCommon.StaticTopicNameTestResolver{TopicName: "test"}
|
||||||
|
err := Send(topicNameResolver, nil, context.Background(), nil, nil)
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.ErrMessagingApiNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Send_CloudEventNil(t *testing.T) {
|
||||||
|
var topicNameResolver pkgAuditCommon.TopicNameResolver = &pkgAuditCommon.StaticTopicNameTestResolver{TopicName: "test"}
|
||||||
|
var messagingApi pkgMessagingApi.Api = &pkgMessagingApi.AmqpApi{}
|
||||||
|
|
||||||
|
err := Send(topicNameResolver, messagingApi, context.Background(), nil, nil)
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.ErrCloudEventNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Send_ObjectIdentifierNil(t *testing.T) {
|
||||||
|
var topicNameResolver pkgAuditCommon.TopicNameResolver = &pkgAuditCommon.StaticTopicNameTestResolver{TopicName: "test"}
|
||||||
|
var messagingApi pkgMessagingApi.Api = &pkgMessagingApi.AmqpApi{}
|
||||||
|
var cloudEvent = pkgAuditCommon.CloudEvent{}
|
||||||
|
|
||||||
|
err := Send(topicNameResolver, messagingApi, context.Background(), nil, &cloudEvent)
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.ErrObjectIdentifierNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Send_UnsupportedObjectIdentifierType(t *testing.T) {
|
||||||
|
var topicNameResolver pkgAuditCommon.TopicNameResolver = &pkgAuditCommon.StaticTopicNameTestResolver{TopicName: "test"}
|
||||||
|
var messagingApi pkgMessagingApi.Api = &pkgMessagingApi.AmqpApi{}
|
||||||
|
var cloudEvent = pkgAuditCommon.CloudEvent{}
|
||||||
|
var objectIdentifier = auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: "unsupported"}
|
||||||
|
|
||||||
|
err := Send(topicNameResolver, messagingApi, context.Background(), pkgAuditCommon.NewRoutableIdentifier(&objectIdentifier), &cloudEvent)
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.ErrUnsupportedRoutableType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Send(t *testing.T) {
|
||||||
|
topicNameResolverMock := TopicNameResolverMock{}
|
||||||
|
topicNameResolverMock.On("Resolve", mock.Anything).Return("topic", nil)
|
||||||
|
var topicNameResolver pkgAuditCommon.TopicNameResolver = &topicNameResolverMock
|
||||||
|
|
||||||
|
messagingApiMock := MessagingApiMock{}
|
||||||
|
messagingApiMock.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||||
|
var messagingApi pkgMessagingApi.Api = &messagingApiMock
|
||||||
|
|
||||||
|
var cloudEvent = pkgAuditCommon.CloudEvent{}
|
||||||
|
assert.NoError(t, Send(topicNameResolver, messagingApi, context.Background(), pkgAuditCommon.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 pkgAuditCommon.TopicNameResolver = &topicNameResolverMock
|
||||||
|
|
||||||
|
messagingApiMock := MessagingApiMock{}
|
||||||
|
messagingApiMock.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||||
|
var messagingApi pkgMessagingApi.Api = &messagingApiMock
|
||||||
|
|
||||||
|
traceState := "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"
|
||||||
|
traceParent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
|
||||||
|
expectedTime := time.Now()
|
||||||
|
var cloudEvent = pkgAuditCommon.CloudEvent{
|
||||||
|
SpecVersion: "1.0",
|
||||||
|
Source: "resourcemanager",
|
||||||
|
Id: "id",
|
||||||
|
Time: expectedTime,
|
||||||
|
DataContentType: pkgAuditCommon.ContentTypeCloudEventsProtobuf,
|
||||||
|
DataType: "type",
|
||||||
|
Subject: "subject",
|
||||||
|
TraceParent: &traceParent,
|
||||||
|
TraceState: &traceState,
|
||||||
|
}
|
||||||
|
assert.NoError(t, Send(topicNameResolver, messagingApi, context.Background(), pkgAuditCommon.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, pkgAuditCommon.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, pkgAuditCommon.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 pkgAuditCommon.TopicNameResolver = &topicNameResolverMock
|
||||||
|
|
||||||
|
messagingApiMock := MessagingApiMock{}
|
||||||
|
messagingApiMock.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||||
|
var messagingApi pkgMessagingApi.Api = &messagingApiMock
|
||||||
|
|
||||||
|
expectedTime := time.Now()
|
||||||
|
var cloudEvent = pkgAuditCommon.CloudEvent{
|
||||||
|
SpecVersion: "1.0",
|
||||||
|
Source: "resourcemanager",
|
||||||
|
Id: "id",
|
||||||
|
Time: expectedTime,
|
||||||
|
DataContentType: pkgAuditCommon.ContentTypeCloudEventsProtobuf,
|
||||||
|
DataType: "type",
|
||||||
|
Subject: "subject",
|
||||||
|
}
|
||||||
|
assert.NoError(t, Send(topicNameResolver, messagingApi, context.Background(), pkgAuditCommon.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, pkgAuditCommon.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, pkgAuditCommon.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ValidateTopicNames(t *testing.T) {
|
||||||
|
t.Run("conway", func(t *testing.T) {
|
||||||
|
topicName := "topic://stackit-platform/t/swz/audit-log/conway/v1.0/service-name/events"
|
||||||
|
assert.True(t, TopicNamePattern.MatchString(topicName))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("eu01", func(t *testing.T) {
|
||||||
|
topicName := "topic://stackit-platform/t/swz/audit-log/eu01/v1.0/service-name/events"
|
||||||
|
assert.True(t, TopicNamePattern.MatchString(topicName))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("eu02", func(t *testing.T) {
|
||||||
|
topicName := "topic://stackit-platform/t/swz/audit-log/eu02/v1.0/service-name/events"
|
||||||
|
assert.True(t, TopicNamePattern.MatchString(topicName))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sx-stoi01", func(t *testing.T) {
|
||||||
|
topicName := "topic://stackit-platform/t/swz/audit-log/sx-stoi01/v1.0/service-name/events"
|
||||||
|
assert.True(t, TopicNamePattern.MatchString(topicName))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("version without decimals", func(t *testing.T) {
|
||||||
|
topicName := "topic://stackit-platform/t/swz/audit-log/eu01/v1/service-name/events"
|
||||||
|
assert.True(t, TopicNamePattern.MatchString(topicName))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("version as uppercase", func(t *testing.T) {
|
||||||
|
topicName := "topic://stackit-platform/t/swz/audit-log/eu01/V1.0/service-name/events"
|
||||||
|
assert.True(t, TopicNamePattern.MatchString(topicName))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("service name without dash", func(t *testing.T) {
|
||||||
|
topicName := "topic://stackit-platform/t/swz/audit-log/eu01/v1.0/service/events"
|
||||||
|
assert.True(t, TopicNamePattern.MatchString(topicName))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("multiple additional parts", func(t *testing.T) {
|
||||||
|
topicName := "topic://stackit-platform/t/swz/audit-log/eu01/v1.0/service-name/multiple/additional/parts"
|
||||||
|
assert.True(t, TopicNamePattern.MatchString(topicName))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("additional parts with dash", func(t *testing.T) {
|
||||||
|
topicName := "topic://stackit-platform/t/swz/audit-log/eu01/v1.0/service-name/multiple-additional/parts"
|
||||||
|
assert.True(t, TopicNamePattern.MatchString(topicName))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("topic prefix missing", func(t *testing.T) {
|
||||||
|
topicName := "stackit-platform/t/swz/audit-log/eu01/v1.0/service-name/events"
|
||||||
|
assert.False(t, TopicNamePattern.MatchString(topicName))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid region", func(t *testing.T) {
|
||||||
|
topicName := "topic://stackit-platform/t/swz/audit-log/invalid/v1.0/service-name/events"
|
||||||
|
assert.False(t, TopicNamePattern.MatchString(topicName))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("additional parts missing", func(t *testing.T) {
|
||||||
|
topicName := "topic://stackit-platform/t/swz/audit-log/eu01/v1.0/service-name"
|
||||||
|
assert.False(t, TopicNamePattern.MatchString(topicName))
|
||||||
|
})
|
||||||
|
}
|
||||||
310
internal/audit/api/api_legacy_converter.go
Normal file
310
internal/audit/api/api_legacy_converter.go
Normal file
|
|
@ -0,0 +1,310 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(event.LogName, string(pkgAuditCommon.EventTypeAdminActivity)):
|
||||||
|
eventType = "ADMIN_ACTIVITY"
|
||||||
|
case strings.HasSuffix(event.LogName, string(pkgAuditCommon.EventTypeSystemEvent)):
|
||||||
|
eventType = "SYSTEM_EVENT"
|
||||||
|
case strings.HasSuffix(event.LogName, string(pkgAuditCommon.EventTypePolicyDenied)):
|
||||||
|
eventType = "POLICY_DENIED"
|
||||||
|
case strings.HasSuffix(event.LogName, string(pkgAuditCommon.EventTypeDataAccess)):
|
||||||
|
return nil, pkgAuditCommon.ErrUnsupportedEventTypeDataAccess
|
||||||
|
default:
|
||||||
|
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
|
||||||
|
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{}
|
||||||
|
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{}
|
||||||
|
if event.ProtoPayload.Request != nil {
|
||||||
|
body = event.ProtoPayload.Request.AsMap()
|
||||||
|
}
|
||||||
|
var headers map[string]interface{}
|
||||||
|
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, pkgAuditCommon.ErrObjectIdentifierNil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context and event type
|
||||||
|
var messageContext *LegacyAuditEventContext
|
||||||
|
switch routableEvent.ObjectIdentifier.Type {
|
||||||
|
case string(pkgAuditCommon.ObjectTypeProject):
|
||||||
|
messageContext = &LegacyAuditEventContext{
|
||||||
|
OrganizationId: nil,
|
||||||
|
FolderId: nil,
|
||||||
|
ProjectId: &routableEvent.ObjectIdentifier.Identifier,
|
||||||
|
}
|
||||||
|
case string(pkgAuditCommon.ObjectTypeFolder):
|
||||||
|
messageContext = &LegacyAuditEventContext{
|
||||||
|
OrganizationId: nil,
|
||||||
|
FolderId: &routableEvent.ObjectIdentifier.Identifier,
|
||||||
|
ProjectId: nil,
|
||||||
|
}
|
||||||
|
case string(pkgAuditCommon.ObjectTypeOrganization):
|
||||||
|
messageContext = &LegacyAuditEventContext{
|
||||||
|
OrganizationId: &routableEvent.ObjectIdentifier.Identifier,
|
||||||
|
FolderId: nil,
|
||||||
|
ProjectId: nil,
|
||||||
|
}
|
||||||
|
case string(pkgAuditCommon.ObjectTypeSystem):
|
||||||
|
messageContext = nil
|
||||||
|
default:
|
||||||
|
return nil, pkgAuditCommon.ErrUnsupportedObjectIdentifierType
|
||||||
|
}
|
||||||
|
|
||||||
|
var visibility string
|
||||||
|
switch routableEvent.Visibility {
|
||||||
|
case auditV1.Visibility_VISIBILITY_PUBLIC:
|
||||||
|
visibility = "PUBLIC"
|
||||||
|
case auditV1.Visibility_VISIBILITY_PRIVATE:
|
||||||
|
visibility = "PRIVATE"
|
||||||
|
case auditV1.Visibility_VISIBILITY_UNSPECIFIED:
|
||||||
|
visibility = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
auditV1.LogSeverity_LOG_SEVERITY_DEBUG,
|
||||||
|
auditV1.LogSeverity_LOG_SEVERITY_INFO,
|
||||||
|
auditV1.LogSeverity_LOG_SEVERITY_NOTICE,
|
||||||
|
auditV1.LogSeverity_LOG_SEVERITY_WARNING:
|
||||||
|
severity = "INFO"
|
||||||
|
case auditV1.LogSeverity_LOG_SEVERITY_ERROR,
|
||||||
|
auditV1.LogSeverity_LOG_SEVERITY_CRITICAL,
|
||||||
|
auditV1.LogSeverity_LOG_SEVERITY_ALERT,
|
||||||
|
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"`
|
||||||
|
}
|
||||||
23
internal/audit/api/api_legacy_converter_test.go
Normal file
23
internal/audit/api/api_legacy_converter_test.go
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
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, pkgAuditCommon.ErrObjectIdentifierNil)
|
||||||
|
}
|
||||||
755
internal/audit/api/model.go
Normal file
755
internal/audit/api/model.go
Normal file
|
|
@ -0,0 +1,755 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||||
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const EmailAddressDoNotReplyAtStackItDotCloud = "do-not-reply@stackit.cloud"
|
||||||
|
const TokenClaimStackitProjectId = "stackit/project/project.id"
|
||||||
|
const TokenClaimStackitServiceAccountId = "stackit/serviceaccount/service-account.uid"
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
// 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 *pkgAuditCommon.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,
|
||||||
|
) (*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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
if auditMetadata.AuditLabels != nil {
|
||||||
|
labels = *auditMetadata.AuditLabels
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize metadata/details
|
||||||
|
var metadata *structpb.Struct
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
return &event, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 *pkgAuditCommon.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 *pkgAuditCommon.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
|
||||||
|
if rawQuery != nil && *rawQuery != "" {
|
||||||
|
escapedQuery := url.QueryEscape(*rawQuery)
|
||||||
|
query = &escapedQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
return &auditV1.AttributeContext_Request{
|
||||||
|
Id: requestId,
|
||||||
|
Method: pkgAuditCommon.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, 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, 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, responseSize *int64, headers map[string]string, responseTime time.Time) *auditV1.ResponseMetadata {
|
||||||
|
|
||||||
|
var message *string
|
||||||
|
if statusCode >= 400 && statusCode < 500 {
|
||||||
|
text := "Client error"
|
||||||
|
message = &text
|
||||||
|
} else if statusCode >= 500 {
|
||||||
|
text := "Server error"
|
||||||
|
message = &text
|
||||||
|
}
|
||||||
|
|
||||||
|
var size *wrapperspb.Int64Value
|
||||||
|
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 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 *pkgAuditCommon.ApiRequest) (*structpb.Struct, error) {
|
||||||
|
|
||||||
|
if 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", "B3" and "Host" headers as well as
|
||||||
|
// all headers starting with the prefixes "X-", "STACKIT-" and "grpcgateway-".
|
||||||
|
// 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", "host"}
|
||||||
|
skipPrefixHeaders := []string{"x-", "stackit-", "grpcgateway-"}
|
||||||
|
|
||||||
|
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 pkgAuditCommon.ObjectType) *pkgAuditCommon.RoutableIdentifier {
|
||||||
|
return &pkgAuditCommon.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 *pkgAuditCommon.ApiRequest) (
|
||||||
|
*structpb.Struct,
|
||||||
|
string,
|
||||||
|
[]string,
|
||||||
|
*auditV1.AuthenticationInfo,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
|
||||||
|
var authenticationPrincipal = "none/none"
|
||||||
|
var principalId = "none"
|
||||||
|
var principalEmail *string
|
||||||
|
emptyClaims, err := structpb.NewStruct(make(map[string]interface{}))
|
||||||
|
if err != nil {
|
||||||
|
return nil, authenticationPrincipal, nil, nil, err
|
||||||
|
}
|
||||||
|
var auditClaims = emptyClaims
|
||||||
|
var serviceAccountName *string
|
||||||
|
audiences := make([]string, 0)
|
||||||
|
var delegationInfo []*auditV1.ServiceAccountDelegationInfo
|
||||||
|
|
||||||
|
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 trimmedAuthorizationHeader != "" {
|
||||||
|
|
||||||
|
// Parse claims
|
||||||
|
token, err := parseToken(trimmedAuthorizationHeader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, authenticationPrincipal, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredClaims, err := parseClaimsFromAuthorizationHeader(token)
|
||||||
|
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(token)
|
||||||
|
principalId, principalEmail = extractSubjectAndEmail(token)
|
||||||
|
|
||||||
|
// Extract service account delegation info data
|
||||||
|
actClaim, hasActClaim := token.Get("act")
|
||||||
|
if hasActClaim {
|
||||||
|
actMap := map[string]interface{}{"act": actClaim}
|
||||||
|
delegationInfo = extractServiceAccountDelegationInfo(actMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract audiences data
|
||||||
|
audiences = token.Audience()
|
||||||
|
|
||||||
|
// Extract project id and service account id
|
||||||
|
projectId := getTokenClaim(token, TokenClaimStackitProjectId)
|
||||||
|
serviceAccountId := getTokenClaim(token, TokenClaimStackitServiceAccountId)
|
||||||
|
|
||||||
|
// Calculate service account name if project and service account ids 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 getTokenClaim(token jwt.Token, claimName string) *string {
|
||||||
|
claim, claimExists := token.Get(claimName)
|
||||||
|
if claimExists {
|
||||||
|
claimString := fmt.Sprintf("%s", claim)
|
||||||
|
return &claimString
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractAuthenticationPrincipal(token jwt.Token) string {
|
||||||
|
subject := token.Subject()
|
||||||
|
issuer := token.Issuer()
|
||||||
|
|
||||||
|
var principal = "none/none"
|
||||||
|
if subject != "" && issuer != "" {
|
||||||
|
principal = fmt.Sprintf("%s/%s", url.QueryEscape(subject), url.QueryEscape(issuer))
|
||||||
|
}
|
||||||
|
return principal
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseToken(authorizationHeader string) (jwt.Token, error) {
|
||||||
|
parts := strings.Split(authorizationHeader, " ")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return nil, ErrInvalidAuthorizationHeaderValue
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(parts[0], "Bearer") {
|
||||||
|
return nil, ErrTokenIsNotBearerToken
|
||||||
|
}
|
||||||
|
jwtString := parts[1]
|
||||||
|
authorizationHeaderParts := strings.Split(jwtString, ".")
|
||||||
|
|
||||||
|
if len(authorizationHeaderParts) == 3 {
|
||||||
|
token, err := jwt.ParseInsecure([]byte(parts[1]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrInvalidBearerToken
|
||||||
|
}
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
return nil, ErrInvalidBearerToken
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseClaimsFromAuthorizationHeader(token jwt.Token) (map[string]interface{}, error) {
|
||||||
|
claimsMap, err := token.AsMap(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := map[string]interface{}{}
|
||||||
|
if len(token.Audience()) > 0 {
|
||||||
|
var audiences []any
|
||||||
|
for _, audience := range token.Audience() {
|
||||||
|
audiences = append(audiences, audience)
|
||||||
|
}
|
||||||
|
claims["aud"] = audiences
|
||||||
|
}
|
||||||
|
|
||||||
|
for key := range claimsMap {
|
||||||
|
if key == "aud" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value := claimsMap[key]
|
||||||
|
t, isTime := value.(time.Time)
|
||||||
|
if isTime {
|
||||||
|
claims[key] = t.String()
|
||||||
|
} else if value != nil && value != "" {
|
||||||
|
claims[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractServiceAccountDelegationInfoDetails(actClaims map[string]interface{}) []*auditV1.ServiceAccountDelegationInfo {
|
||||||
|
principalId, principalEmail := extractSubjectAndEmailFromActClaims(actClaims)
|
||||||
|
|
||||||
|
delegation := auditV1.ServiceAccountDelegationInfo{Authority: &auditV1.ServiceAccountDelegationInfo_IdpPrincipal_{IdpPrincipal: &auditV1.ServiceAccountDelegationInfo_IdpPrincipal{
|
||||||
|
PrincipalId: principalId,
|
||||||
|
PrincipalEmail: principalEmail,
|
||||||
|
ServiceMetadata: nil,
|
||||||
|
}}}
|
||||||
|
|
||||||
|
delegations := []*auditV1.ServiceAccountDelegationInfo{&delegation}
|
||||||
|
nestedDelegations := extractServiceAccountDelegationInfo(actClaims)
|
||||||
|
if len(nestedDelegations) > 0 {
|
||||||
|
return append(delegations, nestedDelegations...)
|
||||||
|
}
|
||||||
|
return delegations
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractServiceAccountDelegationInfo(claims map[string]interface{}) []*auditV1.ServiceAccountDelegationInfo {
|
||||||
|
actor, hasActor := claims["act"]
|
||||||
|
if hasActor {
|
||||||
|
actorMap, hasActorClaim := actor.(map[string]interface{})
|
||||||
|
if hasActorClaim {
|
||||||
|
return extractServiceAccountDelegationInfoDetails(actorMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractSubjectAndEmailFromActClaims(actClaim map[string]interface{}) (string, string) {
|
||||||
|
var principalEmail string
|
||||||
|
principalId := fmt.Sprintf("%s", actClaim["sub"])
|
||||||
|
principalEmailRaw := actClaim["email"]
|
||||||
|
if principalEmailRaw == nil {
|
||||||
|
principalEmail = EmailAddressDoNotReplyAtStackItDotCloud
|
||||||
|
} else {
|
||||||
|
principalEmail = fmt.Sprintf("%s", principalEmailRaw)
|
||||||
|
}
|
||||||
|
return principalId, principalEmail
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractSubjectAndEmail(token jwt.Token) (string, *string) {
|
||||||
|
var principalEmail *string
|
||||||
|
principalId := token.Subject()
|
||||||
|
emailClaim, hasEmail := token.Get("email")
|
||||||
|
if hasEmail {
|
||||||
|
trimmedEmail := strings.TrimSpace(fmt.Sprintf("%s", emailClaim))
|
||||||
|
if trimmedEmail != "" {
|
||||||
|
principalEmail = &trimmedEmail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return principalId, principalEmail
|
||||||
|
}
|
||||||
931
internal/audit/api/model_test.go
Normal file
931
internal/audit/api/model_test.go
Normal file
|
|
@ -0,0 +1,931 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_NewPbInt64Value(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("nil", func(t *testing.T) {
|
||||||
|
value := NewPbInt64Value(nil)
|
||||||
|
assert.Nil(t, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("value", func(t *testing.T) {
|
||||||
|
var input int64 = 1
|
||||||
|
value := NewPbInt64Value(&input)
|
||||||
|
assert.Equal(t, wrapperspb.Int64Value{Value: 1}.Value, value.Value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_NewResponseMetadata(t *testing.T) {
|
||||||
|
|
||||||
|
headers := make(map[string]string)
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
responseTime := time.Now().UTC()
|
||||||
|
responseItems := int64(10)
|
||||||
|
responseSize := int64(100)
|
||||||
|
|
||||||
|
t.Run("no error", func(t *testing.T) {
|
||||||
|
for code := 1; code < 400; code++ {
|
||||||
|
metadata := NewResponseMetadata(code, &responseItems, &responseSize, headers, responseTime)
|
||||||
|
assert.Equal(t, wrapperspb.Int32(int32(code)).Value, metadata.StatusCode.Value)
|
||||||
|
assert.Nil(t, metadata.ErrorMessage)
|
||||||
|
assert.Nil(t, metadata.ErrorDetails)
|
||||||
|
assert.Equal(t, wrapperspb.Int64(responseItems), metadata.ResponseAttributes.NumResponseItems)
|
||||||
|
assert.Equal(t, wrapperspb.Int64(responseSize), metadata.ResponseAttributes.Size)
|
||||||
|
assert.Equal(t, timestamppb.New(responseTime), metadata.ResponseAttributes.Time)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("client error", func(t *testing.T) {
|
||||||
|
for code := 400; code < 500; code++ {
|
||||||
|
metadata := NewResponseMetadata(code, nil, nil, headers, responseTime)
|
||||||
|
assert.Equal(t, wrapperspb.Int32(int32(code)).Value, metadata.StatusCode.Value)
|
||||||
|
assert.Equal(t, "Client error", *metadata.ErrorMessage)
|
||||||
|
assert.Nil(t, metadata.ErrorDetails)
|
||||||
|
assert.Nil(t, metadata.ResponseAttributes.NumResponseItems)
|
||||||
|
assert.Nil(t, metadata.ResponseAttributes.Size)
|
||||||
|
assert.Equal(t, timestamppb.New(responseTime), metadata.ResponseAttributes.Time)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("server error", func(t *testing.T) {
|
||||||
|
for code := 500; code < 600; code++ {
|
||||||
|
metadata := NewResponseMetadata(code, nil, nil, headers, responseTime)
|
||||||
|
assert.Equal(t, wrapperspb.Int32(int32(code)).Value, metadata.StatusCode.Value)
|
||||||
|
assert.Equal(t, "Server error", *metadata.ErrorMessage)
|
||||||
|
assert.Nil(t, metadata.ErrorDetails)
|
||||||
|
assert.Nil(t, metadata.ResponseAttributes.NumResponseItems)
|
||||||
|
assert.Nil(t, metadata.ResponseAttributes.Size)
|
||||||
|
assert.Equal(t, timestamppb.New(responseTime), metadata.ResponseAttributes.Time)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_NewRequestMetadata(t *testing.T) {
|
||||||
|
|
||||||
|
userAgent := "userAgent"
|
||||||
|
requestHeaders := make(map[string][]string)
|
||||||
|
requestHeaders["User-Agent"] = []string{userAgent}
|
||||||
|
requestHeaders["Custom"] = []string{"customHeader"}
|
||||||
|
|
||||||
|
queryString := "topic=project"
|
||||||
|
request := pkgAuditCommon.ApiRequest{
|
||||||
|
Method: "GET",
|
||||||
|
URL: pkgAuditCommon.RequestUrl{Path: "/audit/new", RawQuery: &queryString},
|
||||||
|
Host: "localhost:8080",
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
Scheme: "http",
|
||||||
|
Header: requestHeaders,
|
||||||
|
}
|
||||||
|
|
||||||
|
requestId := "requestId"
|
||||||
|
requestScheme := "requestScheme"
|
||||||
|
requestTime := time.Now().UTC()
|
||||||
|
|
||||||
|
audiences := []string{"audience"}
|
||||||
|
authenticationPrincipal := "authenticationPrincipal"
|
||||||
|
|
||||||
|
claimMap := make(map[string]interface{})
|
||||||
|
auditClaims, _ := structpb.NewStruct(claimMap)
|
||||||
|
|
||||||
|
clientIp := "clientIp"
|
||||||
|
|
||||||
|
filteredHeaders := make(map[string]string)
|
||||||
|
filteredHeaders["Custom"] = "customHeader"
|
||||||
|
filteredHeaders["User-Agent"] = userAgent
|
||||||
|
|
||||||
|
verifyRequestMetadata := func(requestMetadata *auditV1.RequestMetadata, requestId *string) {
|
||||||
|
assert.Equal(t, clientIp, requestMetadata.CallerIp)
|
||||||
|
assert.Equal(t, userAgent, requestMetadata.CallerSuppliedUserAgent)
|
||||||
|
assert.NotNil(t, requestMetadata.RequestAttributes)
|
||||||
|
|
||||||
|
attributes := requestMetadata.RequestAttributes
|
||||||
|
assert.Equal(t, requestId, attributes.Id)
|
||||||
|
assert.Equal(t, filteredHeaders, attributes.Headers)
|
||||||
|
assert.Equal(t, request.URL.Path, attributes.Path)
|
||||||
|
assert.Equal(t, request.Host, attributes.Host)
|
||||||
|
assert.Equal(t, requestScheme, attributes.Scheme)
|
||||||
|
assert.Equal(t, timestamppb.New(requestTime), attributes.Time)
|
||||||
|
assert.Equal(t, request.Proto, attributes.Protocol)
|
||||||
|
assert.NotNil(t, attributes.Auth)
|
||||||
|
|
||||||
|
auth := attributes.Auth
|
||||||
|
assert.Equal(t, authenticationPrincipal, auth.Principal)
|
||||||
|
assert.Equal(t, audiences, auth.Audiences)
|
||||||
|
assert.Equal(t, auditClaims, auth.Claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("with query parameters", func(t *testing.T) {
|
||||||
|
requestMetadata := NewRequestMetadata(
|
||||||
|
&request,
|
||||||
|
filteredHeaders,
|
||||||
|
&requestId,
|
||||||
|
requestScheme,
|
||||||
|
requestTime,
|
||||||
|
clientIp,
|
||||||
|
authenticationPrincipal,
|
||||||
|
audiences,
|
||||||
|
auditClaims,
|
||||||
|
)
|
||||||
|
|
||||||
|
verifyRequestMetadata(requestMetadata, &requestId)
|
||||||
|
assert.Equal(t, "topic%3Dproject", *requestMetadata.RequestAttributes.Query)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("without query parameters", func(t *testing.T) {
|
||||||
|
request := pkgAuditCommon.ApiRequest{
|
||||||
|
Method: "GET",
|
||||||
|
URL: pkgAuditCommon.RequestUrl{Path: "/audit/new"},
|
||||||
|
Host: "localhost:8080",
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
Header: requestHeaders,
|
||||||
|
}
|
||||||
|
|
||||||
|
requestMetadata := NewRequestMetadata(
|
||||||
|
&request,
|
||||||
|
filteredHeaders,
|
||||||
|
&requestId,
|
||||||
|
requestScheme,
|
||||||
|
requestTime,
|
||||||
|
clientIp,
|
||||||
|
authenticationPrincipal,
|
||||||
|
audiences,
|
||||||
|
auditClaims,
|
||||||
|
)
|
||||||
|
|
||||||
|
verifyRequestMetadata(requestMetadata, &requestId)
|
||||||
|
assert.Nil(t, requestMetadata.RequestAttributes.Query)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("with empty query parameters", func(t *testing.T) {
|
||||||
|
emptyQuery := ""
|
||||||
|
request := pkgAuditCommon.ApiRequest{
|
||||||
|
Method: "GET",
|
||||||
|
URL: pkgAuditCommon.RequestUrl{Path: "/audit/new", RawQuery: &emptyQuery},
|
||||||
|
Host: "localhost:8080",
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
Header: requestHeaders,
|
||||||
|
}
|
||||||
|
|
||||||
|
requestMetadata := NewRequestMetadata(
|
||||||
|
&request,
|
||||||
|
filteredHeaders,
|
||||||
|
&requestId,
|
||||||
|
requestScheme,
|
||||||
|
requestTime,
|
||||||
|
clientIp,
|
||||||
|
authenticationPrincipal,
|
||||||
|
audiences,
|
||||||
|
auditClaims,
|
||||||
|
)
|
||||||
|
|
||||||
|
verifyRequestMetadata(requestMetadata, &requestId)
|
||||||
|
assert.Nil(t, requestMetadata.RequestAttributes.Query)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("without request id", func(t *testing.T) {
|
||||||
|
request := pkgAuditCommon.ApiRequest{
|
||||||
|
Method: "GET",
|
||||||
|
URL: pkgAuditCommon.RequestUrl{Path: "/audit/new", RawQuery: &queryString},
|
||||||
|
Host: "localhost:8080",
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
Header: requestHeaders,
|
||||||
|
}
|
||||||
|
|
||||||
|
requestMetadata := NewRequestMetadata(
|
||||||
|
&request, filteredHeaders,
|
||||||
|
nil,
|
||||||
|
requestScheme,
|
||||||
|
requestTime,
|
||||||
|
clientIp,
|
||||||
|
authenticationPrincipal,
|
||||||
|
audiences,
|
||||||
|
auditClaims,
|
||||||
|
)
|
||||||
|
verifyRequestMetadata(requestMetadata, nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("various default http methods", func(t *testing.T) {
|
||||||
|
httpMethods := []string{"GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"}
|
||||||
|
for _, httpMethod := range httpMethods {
|
||||||
|
request := pkgAuditCommon.ApiRequest{
|
||||||
|
Method: httpMethod,
|
||||||
|
URL: pkgAuditCommon.RequestUrl{Path: "/audit/new", RawQuery: &queryString},
|
||||||
|
Host: "localhost:8080",
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
Header: requestHeaders,
|
||||||
|
}
|
||||||
|
|
||||||
|
requestMetadata := NewRequestMetadata(
|
||||||
|
&request, filteredHeaders,
|
||||||
|
&requestId, requestScheme,
|
||||||
|
requestTime, clientIp,
|
||||||
|
authenticationPrincipal,
|
||||||
|
audiences,
|
||||||
|
auditClaims,
|
||||||
|
)
|
||||||
|
|
||||||
|
verifyRequestMetadata(requestMetadata, &requestId)
|
||||||
|
expectedMethod := fmt.Sprintf("HTTP_METHOD_%s", httpMethod)
|
||||||
|
assert.Equal(t, expectedMethod, requestMetadata.RequestAttributes.Method.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unknown http method", func(t *testing.T) {
|
||||||
|
request := pkgAuditCommon.ApiRequest{
|
||||||
|
Method: "",
|
||||||
|
URL: pkgAuditCommon.RequestUrl{Path: "/audit/new", RawQuery: &queryString},
|
||||||
|
Host: "localhost:8080",
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
Header: requestHeaders,
|
||||||
|
}
|
||||||
|
|
||||||
|
requestMetadata := NewRequestMetadata(
|
||||||
|
&request, filteredHeaders,
|
||||||
|
&requestId, requestScheme,
|
||||||
|
requestTime, clientIp,
|
||||||
|
authenticationPrincipal,
|
||||||
|
audiences,
|
||||||
|
auditClaims,
|
||||||
|
)
|
||||||
|
|
||||||
|
verifyRequestMetadata(requestMetadata, &requestId)
|
||||||
|
assert.Equal(t,
|
||||||
|
auditV1.AttributeContext_HTTP_METHOD_UNSPECIFIED.String(),
|
||||||
|
requestMetadata.RequestAttributes.Method.String())
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_FilterAndMergeRequestHeaders(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("skip headers", func(t *testing.T) {
|
||||||
|
headers := make(map[string][]string)
|
||||||
|
headers["Authorization"] = []string{"ey..."}
|
||||||
|
headers["authorization"] = []string{"ey..."}
|
||||||
|
headers["B3"] = []string{"80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-1-05e3ac9a4f6e3b90"}
|
||||||
|
headers["b3"] = []string{"80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-1-05e3ac9a4f6e3b90"}
|
||||||
|
headers["Host"] = []string{"localhost:9090"}
|
||||||
|
headers["host"] = []string{"localhost:9090"}
|
||||||
|
headers[":authority"] = []string{"localhost:9090"}
|
||||||
|
|
||||||
|
filteredHeaders := FilterAndMergeHeaders(headers)
|
||||||
|
assert.Equal(t, 0, len(filteredHeaders))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("skip headers by prefix", func(t *testing.T) {
|
||||||
|
headers := make(map[string][]string)
|
||||||
|
headers["X-Forwarded-Proto"] = []string{"https"}
|
||||||
|
headers["Stackit-test"] = []string{"test"}
|
||||||
|
headers["grpcgateway-authorization"] = []string{userToken}
|
||||||
|
|
||||||
|
filteredHeaders := FilterAndMergeHeaders(headers)
|
||||||
|
assert.Equal(t, 0, len(filteredHeaders))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("merge headers", func(t *testing.T) {
|
||||||
|
headers := make(map[string][]string)
|
||||||
|
headers["Custom1"] = []string{"value1", "value2"}
|
||||||
|
headers["Custom2"] = []string{"value3", "value4"}
|
||||||
|
|
||||||
|
filteredHeaders := FilterAndMergeHeaders(headers)
|
||||||
|
assert.Equal(t, 2, len(filteredHeaders))
|
||||||
|
assert.Equal(t, "value1,value2", filteredHeaders["Custom1"])
|
||||||
|
assert.Equal(t, "value3,value4", filteredHeaders["Custom2"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("skip merge headers mixed", func(t *testing.T) {
|
||||||
|
headers := make(map[string][]string)
|
||||||
|
headers["Custom1"] = []string{"value1", "value2"}
|
||||||
|
headers["Custom2"] = []string{"value3"}
|
||||||
|
headers["STACKIT-MIXED"] = []string{"test"}
|
||||||
|
|
||||||
|
filteredHeaders := FilterAndMergeHeaders(headers)
|
||||||
|
assert.Equal(t, 2, len(filteredHeaders))
|
||||||
|
assert.Equal(t, "value1,value2", filteredHeaders["Custom1"])
|
||||||
|
assert.Equal(t, "value3", filteredHeaders["Custom2"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Keep empty and blank header values", func(t *testing.T) {
|
||||||
|
headers := make(map[string][]string)
|
||||||
|
headers["empty"] = []string{""}
|
||||||
|
headers["blank"] = []string{" "}
|
||||||
|
filteredHeaders := FilterAndMergeHeaders(headers)
|
||||||
|
assert.Equal(t, 2, len(filteredHeaders))
|
||||||
|
assert.Equal(t, "", filteredHeaders["empty"])
|
||||||
|
assert.Equal(t, " ", filteredHeaders["blank"])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_AuditAttributesFromAuthorizationHeader(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("basic token", func(t *testing.T) {
|
||||||
|
headerValue := "Basic username:password"
|
||||||
|
headers := make(map[string][]string)
|
||||||
|
headers["Authorization"] = []string{headerValue}
|
||||||
|
request := pkgAuditCommon.ApiRequest{Header: headers}
|
||||||
|
|
||||||
|
_, _, _, _, err := AuditAttributesFromAuthorizationHeader(&request)
|
||||||
|
assert.ErrorIs(t, err, ErrTokenIsNotBearerToken)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid header value", func(t *testing.T) {
|
||||||
|
headerValue := "a b c"
|
||||||
|
headers := make(map[string][]string)
|
||||||
|
headers["Authorization"] = []string{headerValue}
|
||||||
|
request := pkgAuditCommon.ApiRequest{Header: headers}
|
||||||
|
|
||||||
|
_, _, _, _, err := AuditAttributesFromAuthorizationHeader(&request)
|
||||||
|
assert.ErrorIs(t, err, ErrInvalidAuthorizationHeaderValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid token too many parts", func(t *testing.T) {
|
||||||
|
headerValue := "Bearer a.b.c.d"
|
||||||
|
headers := make(map[string][]string)
|
||||||
|
headers["Authorization"] = []string{headerValue}
|
||||||
|
request := pkgAuditCommon.ApiRequest{Header: headers}
|
||||||
|
|
||||||
|
_, _, _, _, err := AuditAttributesFromAuthorizationHeader(&request)
|
||||||
|
assert.ErrorIs(t, err, ErrInvalidBearerToken)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid bearer token", func(t *testing.T) {
|
||||||
|
headerValue := "Bearer a.b.c"
|
||||||
|
headers := make(map[string][]string)
|
||||||
|
headers["Authorization"] = []string{headerValue}
|
||||||
|
request := pkgAuditCommon.ApiRequest{Header: headers}
|
||||||
|
|
||||||
|
_, _, _, _, err := AuditAttributesFromAuthorizationHeader(&request)
|
||||||
|
assert.ErrorIs(t, err, ErrInvalidBearerToken)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("client credentials token", func(t *testing.T) {
|
||||||
|
headers := make(map[string][]string)
|
||||||
|
headers["Authorization"] = []string{clientCredentialsToken}
|
||||||
|
request := pkgAuditCommon.ApiRequest{Header: headers}
|
||||||
|
|
||||||
|
auditClaims, authenticationPrincipal, audiences, authenticationInfo, err :=
|
||||||
|
AuditAttributesFromAuthorizationHeader(&request)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
auditClaimsMap := auditClaims.AsMap()
|
||||||
|
assert.Len(t, auditClaimsMap, 9)
|
||||||
|
assert.Equal(t, []interface{}{"stackit-resource-manager-dev"}, auditClaimsMap["aud"])
|
||||||
|
assert.Equal(t, "stackit-resource-manager-dev", auditClaimsMap["client_id"])
|
||||||
|
assert.Equal(t, "2024-08-23 09:28:46 +0000 UTC", auditClaimsMap["exp"])
|
||||||
|
assert.Equal(t, "2024-08-23 09:13:46 +0000 UTC", auditClaimsMap["iat"])
|
||||||
|
assert.Equal(t, "https://accounts.dev.stackit.cloud", auditClaimsMap["iss"])
|
||||||
|
assert.Equal(t, "e46eba38-dedb-4541-94f3-49f97a934d58", auditClaimsMap["jti"])
|
||||||
|
assert.Equal(t, "2024-08-23 09:13:46 +0000 UTC", auditClaimsMap["nbf"])
|
||||||
|
assert.Equal(t, "uaa.none", auditClaimsMap["scope"])
|
||||||
|
assert.Equal(t, "stackit-resource-manager-dev", auditClaimsMap["sub"])
|
||||||
|
|
||||||
|
principal := fmt.Sprintf("%s/%s",
|
||||||
|
url.QueryEscape("stackit-resource-manager-dev"),
|
||||||
|
url.QueryEscape("https://accounts.dev.stackit.cloud"))
|
||||||
|
assert.Equal(t, principal, authenticationPrincipal)
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"stackit-resource-manager-dev"}, audiences)
|
||||||
|
|
||||||
|
assert.Equal(t, "stackit-resource-manager-dev", authenticationInfo.PrincipalId)
|
||||||
|
assert.Nil(t, authenticationInfo.PrincipalEmail)
|
||||||
|
|
||||||
|
assert.Nil(t, authenticationInfo.ServiceAccountName)
|
||||||
|
assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("service account access token", func(t *testing.T) {
|
||||||
|
headers := make(map[string][]string)
|
||||||
|
headers["Authorization"] = []string{serviceAccountToken}
|
||||||
|
request := pkgAuditCommon.ApiRequest{Header: headers}
|
||||||
|
|
||||||
|
auditClaims, authenticationPrincipal, audiences, authenticationInfo, err :=
|
||||||
|
AuditAttributesFromAuthorizationHeader(&request)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
auditClaimsMap := auditClaims.AsMap()
|
||||||
|
assert.Len(t, auditClaimsMap, 12)
|
||||||
|
assert.Equal(t, []interface{}{"stackit", "api"}, auditClaimsMap["aud"])
|
||||||
|
assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", auditClaimsMap["azp"])
|
||||||
|
assert.Equal(t, "my-service-yifc9e1@sa.stackit.cloud", auditClaimsMap["email"])
|
||||||
|
assert.Equal(t, "2024-08-03 07:15:43 +0000 UTC", auditClaimsMap["exp"])
|
||||||
|
assert.Equal(t, "2024-08-02 07:15:43 +0000 UTC", auditClaimsMap["iat"])
|
||||||
|
assert.Equal(t, "stackit/serviceaccount", auditClaimsMap["iss"])
|
||||||
|
assert.Equal(t, "84c30a46-1001-436f-859f-89c0ba19be1e", auditClaimsMap["jti"])
|
||||||
|
assert.Equal(t, "api", auditClaimsMap["stackit/serviceaccount/namespace"])
|
||||||
|
assert.Equal(t, "10f38b01-534b-47bb-a03a-e294ca2be4de", auditClaimsMap[TokenClaimStackitServiceAccountId])
|
||||||
|
assert.Equal(t, "legacy", auditClaimsMap["stackit/serviceaccount/token.source"])
|
||||||
|
assert.Equal(t, "dacc7830-843e-4c5e-86ff-aa0fb51d636f", auditClaimsMap[TokenClaimStackitProjectId])
|
||||||
|
assert.Equal(t, "10f38b01-534b-47bb-a03a-e294ca2be4de", auditClaimsMap["sub"])
|
||||||
|
|
||||||
|
principal := fmt.Sprintf("%s/%s",
|
||||||
|
url.QueryEscape("10f38b01-534b-47bb-a03a-e294ca2be4de"),
|
||||||
|
url.QueryEscape("stackit/serviceaccount"))
|
||||||
|
assert.Equal(t, principal, authenticationPrincipal)
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"stackit", "api"}, audiences)
|
||||||
|
|
||||||
|
assert.Equal(t, "10f38b01-534b-47bb-a03a-e294ca2be4de", authenticationInfo.PrincipalId)
|
||||||
|
assert.Equal(t, "my-service-yifc9e1@sa.stackit.cloud", *authenticationInfo.PrincipalEmail)
|
||||||
|
|
||||||
|
assert.Equal(t,
|
||||||
|
"projects/dacc7830-843e-4c5e-86ff-aa0fb51d636f/service-accounts/10f38b01-534b-47bb-a03a-e294ca2be4de",
|
||||||
|
*authenticationInfo.ServiceAccountName)
|
||||||
|
assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("service account access token with underscore in subject", func(t *testing.T) {
|
||||||
|
headers := make(map[string][]string)
|
||||||
|
headers["Authorization"] = []string{serviceAccountTokenUnderscoreSubject}
|
||||||
|
request := pkgAuditCommon.ApiRequest{Header: headers}
|
||||||
|
|
||||||
|
auditClaims, authenticationPrincipal, audiences, authenticationInfo, err :=
|
||||||
|
AuditAttributesFromAuthorizationHeader(&request)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
auditClaimsMap := auditClaims.AsMap()
|
||||||
|
assert.Len(t, auditClaimsMap, 12)
|
||||||
|
assert.Equal(t, []interface{}{"stackit", "api"}, auditClaimsMap["aud"])
|
||||||
|
assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", auditClaimsMap["azp"])
|
||||||
|
assert.Equal(t, "my-service-yifc9e1@sa.stackit.cloud", auditClaimsMap["email"])
|
||||||
|
assert.Equal(t, "2024-08-03 07:15:43 +0000 UTC", auditClaimsMap["exp"])
|
||||||
|
assert.Equal(t, "2024-08-02 07:15:43 +0000 UTC", auditClaimsMap["iat"])
|
||||||
|
assert.Equal(t, "stackit/serviceaccount", auditClaimsMap["iss"])
|
||||||
|
assert.Equal(t, "84c30a46-1001-436f-859f-89c0ba19be1e", auditClaimsMap["jti"])
|
||||||
|
assert.Equal(t, "api", auditClaimsMap["stackit/serviceaccount/namespace"])
|
||||||
|
assert.Equal(t, "10f38b01-534b-47bb-a03a-e294ca2be4de", auditClaimsMap[TokenClaimStackitServiceAccountId])
|
||||||
|
assert.Equal(t, "legacy", auditClaimsMap["stackit/serviceaccount/token.source"])
|
||||||
|
assert.Equal(t, "dacc7830-843e-4c5e-86ff-aa0fb51d636f", auditClaimsMap[TokenClaimStackitProjectId])
|
||||||
|
assert.Equal(t, "10f38b01_534b_47bb_a03a_e294ca2be4de", auditClaimsMap["sub"])
|
||||||
|
|
||||||
|
principal := fmt.Sprintf("%s/%s",
|
||||||
|
url.QueryEscape("10f38b01_534b_47bb_a03a_e294ca2be4de"),
|
||||||
|
url.QueryEscape("stackit/serviceaccount"))
|
||||||
|
assert.Equal(t, principal, authenticationPrincipal)
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"stackit", "api"}, audiences)
|
||||||
|
|
||||||
|
assert.Equal(t, "10f38b01_534b_47bb_a03a_e294ca2be4de", authenticationInfo.PrincipalId)
|
||||||
|
assert.Equal(t, "my-service-yifc9e1@sa.stackit.cloud", *authenticationInfo.PrincipalEmail)
|
||||||
|
|
||||||
|
assert.Equal(t,
|
||||||
|
"projects/dacc7830-843e-4c5e-86ff-aa0fb51d636f/service-accounts/10f38b01-534b-47bb-a03a-e294ca2be4de",
|
||||||
|
*authenticationInfo.ServiceAccountName)
|
||||||
|
assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("impersonated token of access token", func(t *testing.T) {
|
||||||
|
headers := make(map[string][]string)
|
||||||
|
headers["Authorization"] = []string{serviceAccountTokenImpersonated}
|
||||||
|
request := pkgAuditCommon.ApiRequest{Header: headers}
|
||||||
|
|
||||||
|
auditClaims, authenticationPrincipal, audiences, authenticationInfo, err :=
|
||||||
|
AuditAttributesFromAuthorizationHeader(&request)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
auditClaimsMap := auditClaims.AsMap()
|
||||||
|
assert.Len(t, auditClaimsMap, 13)
|
||||||
|
assert.Equal(t, []interface{}{"stackit", "api"}, auditClaimsMap["aud"])
|
||||||
|
assert.Equal(t, "02aef516-317f-4ec1-a1df-1acbd4d49fe3", auditClaimsMap["azp"])
|
||||||
|
assert.Equal(t, "service-account-2-tj9srt1@sa.stackit.cloud", auditClaimsMap["email"])
|
||||||
|
assert.Equal(t, "2024-08-19 10:21:47 +0000 UTC", auditClaimsMap["exp"])
|
||||||
|
assert.Equal(t, "2024-08-19 09:21:47 +0000 UTC", auditClaimsMap["iat"])
|
||||||
|
assert.Equal(t, "stackit/serviceaccount", auditClaimsMap["iss"])
|
||||||
|
assert.Equal(t, "37555183-01b9-4270-bdc1-69b4fcfd5ee9", auditClaimsMap["jti"])
|
||||||
|
assert.Equal(t, "api", auditClaimsMap["stackit/serviceaccount/namespace"])
|
||||||
|
assert.Equal(t, "f45009b2-6433-43c1-b6c7-618c44359e71", auditClaimsMap[TokenClaimStackitServiceAccountId])
|
||||||
|
assert.Equal(t, "oauth2", auditClaimsMap["stackit/serviceaccount/token.source"])
|
||||||
|
assert.Equal(t, "dacc7830-843e-4c5e-86ff-aa0fb51d636f", auditClaimsMap[TokenClaimStackitProjectId])
|
||||||
|
assert.Equal(t, "f45009b2-6433-43c1-b6c7-618c44359e71", auditClaimsMap["sub"])
|
||||||
|
assert.NotNil(t, auditClaimsMap["act"])
|
||||||
|
act := auditClaimsMap["act"].(map[string]interface{})
|
||||||
|
assert.NotNil(t, act)
|
||||||
|
assert.Equal(t, "02aef516-317f-4ec1-a1df-1acbd4d49fe3", act["sub"])
|
||||||
|
|
||||||
|
principal := fmt.Sprintf("%s/%s",
|
||||||
|
url.QueryEscape("f45009b2-6433-43c1-b6c7-618c44359e71"),
|
||||||
|
url.QueryEscape("stackit/serviceaccount"))
|
||||||
|
assert.Equal(t, principal, authenticationPrincipal)
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"stackit", "api"}, audiences)
|
||||||
|
|
||||||
|
assert.Equal(t, "f45009b2-6433-43c1-b6c7-618c44359e71", authenticationInfo.PrincipalId)
|
||||||
|
assert.Equal(t, "service-account-2-tj9srt1@sa.stackit.cloud", *authenticationInfo.PrincipalEmail)
|
||||||
|
|
||||||
|
assert.Equal(t,
|
||||||
|
"projects/dacc7830-843e-4c5e-86ff-aa0fb51d636f/service-accounts/f45009b2-6433-43c1-b6c7-618c44359e71",
|
||||||
|
*authenticationInfo.ServiceAccountName)
|
||||||
|
assert.NotNil(t, authenticationInfo.ServiceAccountDelegationInfo)
|
||||||
|
|
||||||
|
serviceAccountDelegationInfo := authenticationInfo.ServiceAccountDelegationInfo
|
||||||
|
assert.Equal(t, "02aef516-317f-4ec1-a1df-1acbd4d49fe3", serviceAccountDelegationInfo[0].GetIdpPrincipal().PrincipalId)
|
||||||
|
assert.Equal(t, "do-not-reply@stackit.cloud", serviceAccountDelegationInfo[0].GetIdpPrincipal().PrincipalEmail)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("impersonated token of impersonated access token", func(t *testing.T) {
|
||||||
|
headers := make(map[string][]string)
|
||||||
|
headers["Authorization"] = []string{serviceAccountTokenRepeatedlyImpersonated}
|
||||||
|
request := pkgAuditCommon.ApiRequest{Header: headers}
|
||||||
|
|
||||||
|
auditClaims, authenticationPrincipal, audiences, authenticationInfo, err :=
|
||||||
|
AuditAttributesFromAuthorizationHeader(&request)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
auditClaimsMap := auditClaims.AsMap()
|
||||||
|
assert.Len(t, auditClaimsMap, 13)
|
||||||
|
assert.Equal(t, []interface{}{"stackit", "api"}, auditClaimsMap["aud"])
|
||||||
|
assert.Equal(t, "f45009b2-6433-43c1-b6c7-618c44359e71", auditClaimsMap["azp"])
|
||||||
|
assert.Equal(t, "service-account-3-fghsxw1@sa.stackit.cloud", auditClaimsMap["email"])
|
||||||
|
assert.Equal(t, "2024-08-19 10:22:43 +0000 UTC", auditClaimsMap["exp"])
|
||||||
|
assert.Equal(t, "2024-08-19 09:22:43 +0000 UTC", auditClaimsMap["iat"])
|
||||||
|
assert.Equal(t, "stackit/serviceaccount", auditClaimsMap["iss"])
|
||||||
|
assert.Equal(t, "1f7f1efc-3349-411a-a5d7-2255e0a5a8ae", auditClaimsMap["jti"])
|
||||||
|
assert.Equal(t, "api", auditClaimsMap["stackit/serviceaccount/namespace"])
|
||||||
|
assert.Equal(t, "1734b4b6-1d5e-4819-9b50-29917a1b9ad5", auditClaimsMap[TokenClaimStackitServiceAccountId])
|
||||||
|
assert.Equal(t, "oauth2", auditClaimsMap["stackit/serviceaccount/token.source"])
|
||||||
|
assert.Equal(t, "dacc7830-843e-4c5e-86ff-aa0fb51d636f", auditClaimsMap[TokenClaimStackitProjectId])
|
||||||
|
assert.Equal(t, "1734b4b6-1d5e-4819-9b50-29917a1b9ad5", auditClaimsMap["sub"])
|
||||||
|
assert.NotNil(t, auditClaimsMap["act"])
|
||||||
|
act := auditClaimsMap["act"].(map[string]interface{})
|
||||||
|
assert.NotNil(t, act)
|
||||||
|
assert.Equal(t, "f45009b2-6433-43c1-b6c7-618c44359e71", act["sub"])
|
||||||
|
nestedAct := act["act"].(map[string]interface{})
|
||||||
|
assert.NotNil(t, nestedAct)
|
||||||
|
assert.Equal(t, "02aef516-317f-4ec1-a1df-1acbd4d49fe3", nestedAct["sub"])
|
||||||
|
|
||||||
|
principal := fmt.Sprintf("%s/%s",
|
||||||
|
url.QueryEscape("1734b4b6-1d5e-4819-9b50-29917a1b9ad5"),
|
||||||
|
url.QueryEscape("stackit/serviceaccount"))
|
||||||
|
assert.Equal(t, principal, authenticationPrincipal)
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"stackit", "api"}, audiences)
|
||||||
|
|
||||||
|
assert.Equal(t, "1734b4b6-1d5e-4819-9b50-29917a1b9ad5", authenticationInfo.PrincipalId)
|
||||||
|
assert.Equal(t, "service-account-3-fghsxw1@sa.stackit.cloud", *authenticationInfo.PrincipalEmail)
|
||||||
|
|
||||||
|
assert.Equal(t,
|
||||||
|
"projects/dacc7830-843e-4c5e-86ff-aa0fb51d636f/service-accounts/1734b4b6-1d5e-4819-9b50-29917a1b9ad5",
|
||||||
|
*authenticationInfo.ServiceAccountName)
|
||||||
|
assert.NotNil(t, authenticationInfo.ServiceAccountDelegationInfo)
|
||||||
|
|
||||||
|
serviceAccountDelegationInfo := authenticationInfo.ServiceAccountDelegationInfo
|
||||||
|
assert.Equal(t, "f45009b2-6433-43c1-b6c7-618c44359e71", serviceAccountDelegationInfo[0].GetIdpPrincipal().PrincipalId)
|
||||||
|
assert.Equal(t, "do-not-reply@stackit.cloud", serviceAccountDelegationInfo[0].GetIdpPrincipal().PrincipalEmail)
|
||||||
|
assert.Equal(t, "02aef516-317f-4ec1-a1df-1acbd4d49fe3", serviceAccountDelegationInfo[1].GetIdpPrincipal().PrincipalId)
|
||||||
|
assert.Equal(t, "do-not-reply@stackit.cloud", serviceAccountDelegationInfo[1].GetIdpPrincipal().PrincipalEmail)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("user token", func(t *testing.T) {
|
||||||
|
headers := make(map[string][]string)
|
||||||
|
headers["Authorization"] = []string{userToken}
|
||||||
|
request := pkgAuditCommon.ApiRequest{Header: headers}
|
||||||
|
|
||||||
|
auditClaims, authenticationPrincipal, audiences, authenticationInfo, err :=
|
||||||
|
AuditAttributesFromAuthorizationHeader(&request)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
auditClaimsMap := auditClaims.AsMap()
|
||||||
|
assert.Len(t, auditClaimsMap, 11)
|
||||||
|
assert.Equal(t, []interface{}{"stackit-portal-login-dev-client-id"}, auditClaimsMap["aud"])
|
||||||
|
assert.Equal(t, "stackit-portal-login-dev-client-id", auditClaimsMap["client_id"])
|
||||||
|
assert.Equal(t, "Christian.Schaible@novatec-gmbh.de", auditClaimsMap["email"])
|
||||||
|
assert.True(t, auditClaimsMap["email_verified"].(bool))
|
||||||
|
assert.Equal(t, "2024-08-02 09:19:27 +0000 UTC", auditClaimsMap["exp"])
|
||||||
|
assert.Equal(t, "2024-08-02 08:19:27 +0000 UTC", auditClaimsMap["iat"])
|
||||||
|
assert.Equal(t, "https://accounts.dev.stackit.cloud", auditClaimsMap["iss"])
|
||||||
|
assert.Equal(t, "d73a67ac-d1ec-4b55-99d4-e953275f022a", auditClaimsMap["jti"])
|
||||||
|
assert.Equal(t, "2024-08-02 08:19:27 +0000 UTC", auditClaimsMap["nbf"])
|
||||||
|
assert.Equal(t, "openid email", auditClaimsMap["scope"])
|
||||||
|
assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", auditClaimsMap["sub"])
|
||||||
|
|
||||||
|
principal := fmt.Sprintf("%s/%s",
|
||||||
|
url.QueryEscape("cd94f01a-df2e-4456-902e-48f5e57f0b63"),
|
||||||
|
url.QueryEscape("https://accounts.dev.stackit.cloud"))
|
||||||
|
assert.Equal(t, principal, authenticationPrincipal)
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"stackit-portal-login-dev-client-id"}, audiences)
|
||||||
|
|
||||||
|
assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", authenticationInfo.PrincipalId)
|
||||||
|
assert.Equal(t, "Christian.Schaible@novatec-gmbh.de", *authenticationInfo.PrincipalEmail)
|
||||||
|
|
||||||
|
assert.Nil(t, authenticationInfo.ServiceAccountName)
|
||||||
|
assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("user token with simple aud claim", func(t *testing.T) {
|
||||||
|
headers := make(map[string][]string)
|
||||||
|
headers["Authorization"] = []string{userTokenWithSimpleAudience}
|
||||||
|
request := pkgAuditCommon.ApiRequest{Header: headers}
|
||||||
|
|
||||||
|
auditClaims, authenticationPrincipal, audiences, authenticationInfo, err :=
|
||||||
|
AuditAttributesFromAuthorizationHeader(&request)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
auditClaimsMap := auditClaims.AsMap()
|
||||||
|
assert.Len(t, auditClaimsMap, 9)
|
||||||
|
assert.Equal(t, []interface{}{"https://stackit-service-account-dev.apps.01.cf.eu01.stackit.cloud"}, auditClaimsMap["aud"])
|
||||||
|
assert.Equal(t, "Lukas.Schmitt@stackit.cloud", auditClaimsMap["email"])
|
||||||
|
assert.Equal(t, "2024-11-21 09:40:35 +0000 UTC", auditClaimsMap["exp"])
|
||||||
|
assert.Equal(t, "2024-11-21 08:40:35 +0000 UTC", auditClaimsMap["iat"])
|
||||||
|
assert.Equal(t, "https://api.dev.stackit.cloud", auditClaimsMap["iss"])
|
||||||
|
assert.Equal(t, "c2be1651-1e54-4e6e-bac3-ef072b3f0149", auditClaimsMap["jti"])
|
||||||
|
assert.Equal(t, "2024-11-21 08:40:18 +0000 UTC", auditClaimsMap["nbf"])
|
||||||
|
assert.Equal(t, "openid email portal-bff", auditClaimsMap["scope"])
|
||||||
|
assert.Equal(t, "5e426aed-c487-4c48-af25-87f69cf9cdd4", auditClaimsMap["sub"])
|
||||||
|
|
||||||
|
principal := fmt.Sprintf("%s/%s",
|
||||||
|
url.QueryEscape("5e426aed-c487-4c48-af25-87f69cf9cdd4"),
|
||||||
|
url.QueryEscape("https://api.dev.stackit.cloud"))
|
||||||
|
assert.Equal(t, principal, authenticationPrincipal)
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"https://stackit-service-account-dev.apps.01.cf.eu01.stackit.cloud"}, audiences)
|
||||||
|
|
||||||
|
assert.Equal(t, "5e426aed-c487-4c48-af25-87f69cf9cdd4", authenticationInfo.PrincipalId)
|
||||||
|
assert.Equal(t, "Lukas.Schmitt@stackit.cloud", *authenticationInfo.PrincipalEmail)
|
||||||
|
|
||||||
|
assert.Nil(t, authenticationInfo.ServiceAccountName)
|
||||||
|
assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_NewAuditLogEntry(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("minimum attributes set", func(t *testing.T) {
|
||||||
|
userAgent := "userAgent"
|
||||||
|
requestHeaders := make(map[string][]string)
|
||||||
|
requestHeaders["Authorization"] = []string{userToken}
|
||||||
|
requestHeaders["User-Agent"] = []string{userAgent}
|
||||||
|
requestHeaders["Custom"] = []string{"customHeader"}
|
||||||
|
|
||||||
|
request := pkgAuditCommon.ApiRequest{
|
||||||
|
Method: "GET",
|
||||||
|
URL: pkgAuditCommon.RequestUrl{Path: "/audit/new"},
|
||||||
|
Host: "localhost:8080",
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
Scheme: "http",
|
||||||
|
Header: requestHeaders,
|
||||||
|
}
|
||||||
|
|
||||||
|
clientIp := "127.0.0.1"
|
||||||
|
correlationId := uuid.NewString()
|
||||||
|
auditRequest := AuditRequest{
|
||||||
|
Request: &request,
|
||||||
|
RequestClientIP: clientIp,
|
||||||
|
RequestCorrelationId: &correlationId,
|
||||||
|
RequestId: nil,
|
||||||
|
RequestTime: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
statusCode := 200
|
||||||
|
auditResponse := AuditResponse{
|
||||||
|
ResponseBodyBytes: nil,
|
||||||
|
ResponseStatusCode: statusCode,
|
||||||
|
ResponseHeaders: nil,
|
||||||
|
ResponseNumItems: nil,
|
||||||
|
ResponseTime: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
objectId := uuid.NewString()
|
||||||
|
logName := fmt.Sprintf("projects/%s/logs/%s", objectId, pkgAuditCommon.EventTypeAdminActivity)
|
||||||
|
serviceName := "resource-manager"
|
||||||
|
operationName := fmt.Sprintf("stackit.%s.v2.projects.updated", serviceName)
|
||||||
|
resourceName := fmt.Sprintf("projects/%s", objectId)
|
||||||
|
auditTime := time.Now().UTC()
|
||||||
|
insertId := fmt.Sprintf("%d/%s/%s/%d", auditTime.UnixNano(), "eu01", "1", 1)
|
||||||
|
|
||||||
|
severity := auditV1.LogSeverity_LOG_SEVERITY_DEFAULT
|
||||||
|
auditMetadata := AuditMetadata{
|
||||||
|
AuditInsertId: insertId,
|
||||||
|
AuditLabels: nil,
|
||||||
|
AuditLogName: logName,
|
||||||
|
AuditLogSeverity: severity,
|
||||||
|
AuditOperationName: operationName,
|
||||||
|
AuditPermission: nil,
|
||||||
|
AuditPermissionGranted: nil,
|
||||||
|
AuditResourceName: resourceName,
|
||||||
|
AuditServiceName: serviceName,
|
||||||
|
AuditTime: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
logEntry, _ := NewAuditLogEntry(
|
||||||
|
auditRequest,
|
||||||
|
auditResponse,
|
||||||
|
nil,
|
||||||
|
auditMetadata)
|
||||||
|
|
||||||
|
assert.Equal(t, logName, logEntry.LogName)
|
||||||
|
assert.Equal(t, insertId, logEntry.InsertId)
|
||||||
|
assert.Equal(t, &correlationId, logEntry.CorrelationId)
|
||||||
|
assert.Equal(t, severity, logEntry.Severity)
|
||||||
|
assert.NoError(t, logEntry.Timestamp.CheckValid())
|
||||||
|
assert.Nil(t, logEntry.Labels)
|
||||||
|
|
||||||
|
payload := logEntry.ProtoPayload
|
||||||
|
assert.NotNil(t, payload)
|
||||||
|
assert.Equal(t, serviceName, payload.ServiceName)
|
||||||
|
assert.Equal(t, operationName, payload.OperationName)
|
||||||
|
assert.Equal(t, resourceName, payload.ResourceName)
|
||||||
|
assert.Equal(t, int32(statusCode), payload.ResponseMetadata.StatusCode.Value)
|
||||||
|
assert.Nil(t, payload.ResponseMetadata.ErrorMessage)
|
||||||
|
assert.Nil(t, payload.ResponseMetadata.ErrorDetails)
|
||||||
|
assert.Nil(t, payload.ResponseMetadata.ResponseAttributes.Size)
|
||||||
|
assert.NotNil(t, payload.ResponseMetadata.ResponseAttributes.Time)
|
||||||
|
assert.Nil(t, payload.ResponseMetadata.ResponseAttributes.NumResponseItems)
|
||||||
|
assert.Nil(t, payload.ResponseMetadata.ResponseAttributes.Headers)
|
||||||
|
|
||||||
|
assert.Nil(t, payload.Request)
|
||||||
|
assert.Nil(t, payload.Response)
|
||||||
|
assert.Nil(t, payload.Metadata)
|
||||||
|
|
||||||
|
authenticationInfo := payload.AuthenticationInfo
|
||||||
|
assert.NotNil(t, authenticationInfo)
|
||||||
|
assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", authenticationInfo.PrincipalId)
|
||||||
|
assert.Equal(t, "Christian.Schaible@novatec-gmbh.de", *authenticationInfo.PrincipalEmail)
|
||||||
|
assert.Nil(t, authenticationInfo.ServiceAccountName)
|
||||||
|
assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
|
||||||
|
|
||||||
|
assert.Nil(t, payload.AuthorizationInfo)
|
||||||
|
|
||||||
|
requestMetadata := payload.RequestMetadata
|
||||||
|
assert.NotNil(t, requestMetadata)
|
||||||
|
assert.Equal(t, clientIp, requestMetadata.CallerIp)
|
||||||
|
assert.Equal(t, userAgent, requestMetadata.CallerSuppliedUserAgent)
|
||||||
|
|
||||||
|
// Don't verify explicitly - trust on other unit test
|
||||||
|
assert.NotNil(t, userAgent, requestMetadata.RequestAttributes)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("all attributes set", func(t *testing.T) {
|
||||||
|
userAgent := "userAgent"
|
||||||
|
requestHeaders := make(map[string][]string)
|
||||||
|
requestHeaders["Authorization"] = []string{userToken}
|
||||||
|
requestHeaders["User-Agent"] = []string{userAgent}
|
||||||
|
requestHeaders["Custom"] = []string{"customHeader"}
|
||||||
|
|
||||||
|
requestBody := make(map[string]interface{})
|
||||||
|
requestBody["key"] = "request"
|
||||||
|
requestBodyBytes, _ := json.Marshal(requestBody)
|
||||||
|
query := "topic=project"
|
||||||
|
request := pkgAuditCommon.ApiRequest{
|
||||||
|
Method: "GET",
|
||||||
|
URL: pkgAuditCommon.RequestUrl{Path: "/audit/new", RawQuery: &query},
|
||||||
|
Host: "localhost:8080",
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
Scheme: "http",
|
||||||
|
Header: requestHeaders,
|
||||||
|
Body: requestBodyBytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
clientIp := "127.0.0.1"
|
||||||
|
correlationId := uuid.NewString()
|
||||||
|
requestId := uuid.NewString()
|
||||||
|
requestTime := time.Now().UTC()
|
||||||
|
auditRequest := AuditRequest{
|
||||||
|
Request: &request,
|
||||||
|
RequestClientIP: clientIp,
|
||||||
|
RequestCorrelationId: &correlationId,
|
||||||
|
RequestId: &requestId,
|
||||||
|
RequestTime: &requestTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
response := make(map[string]interface{})
|
||||||
|
response["key"] = "value"
|
||||||
|
responseBody, _ := json.Marshal(response)
|
||||||
|
responseHeader := http.Header{}
|
||||||
|
responseHeader.Set("Content-Type", "application/json")
|
||||||
|
responseHeaderMap := make(map[string]string)
|
||||||
|
responseHeaderMap["Content-Type"] = "application/json"
|
||||||
|
responseNumItems := int64(1)
|
||||||
|
responseStatusCode := 400
|
||||||
|
responseTime := time.Now().UTC()
|
||||||
|
|
||||||
|
auditResponse := AuditResponse{
|
||||||
|
ResponseBodyBytes: responseBody,
|
||||||
|
ResponseStatusCode: responseStatusCode,
|
||||||
|
ResponseHeaders: responseHeader,
|
||||||
|
ResponseNumItems: &responseNumItems,
|
||||||
|
ResponseTime: &responseTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
auditTime := time.Now().UTC()
|
||||||
|
|
||||||
|
objectId := uuid.NewString()
|
||||||
|
logName := fmt.Sprintf("projects/%s/logs/%s", objectId, pkgAuditCommon.EventTypeAdminActivity)
|
||||||
|
serviceName := "resource-manager"
|
||||||
|
operationName := fmt.Sprintf("stackit.%s.v2.projects.updated", serviceName)
|
||||||
|
resourceName := fmt.Sprintf("projects/%s", objectId)
|
||||||
|
insertId := fmt.Sprintf("%d/%s/%s/%d", auditTime.UnixNano(), "eu01", "1", 1)
|
||||||
|
permission := "resource-manager.project.edit"
|
||||||
|
permissionGranted := true
|
||||||
|
labels := make(map[string]string)
|
||||||
|
labels["label"] = "value"
|
||||||
|
|
||||||
|
severity := auditV1.LogSeverity_LOG_SEVERITY_DEFAULT
|
||||||
|
|
||||||
|
auditMetadata := AuditMetadata{
|
||||||
|
AuditInsertId: insertId,
|
||||||
|
AuditLabels: &labels,
|
||||||
|
AuditLogName: logName,
|
||||||
|
AuditLogSeverity: severity,
|
||||||
|
AuditOperationName: operationName,
|
||||||
|
AuditPermission: &permission,
|
||||||
|
AuditPermissionGranted: &permissionGranted,
|
||||||
|
AuditResourceName: resourceName,
|
||||||
|
AuditServiceName: serviceName,
|
||||||
|
AuditTime: &auditTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
eventMetadata := map[string]interface{}{"key": "value"}
|
||||||
|
logEntry, _ := NewAuditLogEntry(
|
||||||
|
auditRequest,
|
||||||
|
auditResponse,
|
||||||
|
eventMetadata,
|
||||||
|
auditMetadata)
|
||||||
|
|
||||||
|
assert.Equal(t, logName, logEntry.LogName)
|
||||||
|
assert.Equal(t, insertId, logEntry.InsertId)
|
||||||
|
assert.Equal(t, labels, logEntry.Labels)
|
||||||
|
assert.Equal(t, correlationId, *logEntry.CorrelationId)
|
||||||
|
assert.Equal(t, timestamppb.New(auditTime), logEntry.Timestamp)
|
||||||
|
assert.Equal(t, severity, logEntry.Severity)
|
||||||
|
assert.NotNil(t, logEntry.ProtoPayload)
|
||||||
|
|
||||||
|
payload := logEntry.ProtoPayload
|
||||||
|
assert.NotNil(t, payload)
|
||||||
|
assert.Equal(t, serviceName, payload.ServiceName)
|
||||||
|
assert.Equal(t, operationName, payload.OperationName)
|
||||||
|
assert.Equal(t, resourceName, payload.ResourceName)
|
||||||
|
assert.Equal(t, int32(responseStatusCode), payload.ResponseMetadata.StatusCode.Value)
|
||||||
|
assert.Equal(t, "Client error", *payload.ResponseMetadata.ErrorMessage)
|
||||||
|
assert.Nil(t, payload.ResponseMetadata.ErrorDetails)
|
||||||
|
assert.Equal(t, wrapperspb.Int64(int64(len(responseBody))), payload.ResponseMetadata.ResponseAttributes.Size)
|
||||||
|
assert.Equal(t, timestamppb.New(responseTime), payload.ResponseMetadata.ResponseAttributes.Time)
|
||||||
|
assert.Equal(t, wrapperspb.Int64(responseNumItems), payload.ResponseMetadata.ResponseAttributes.NumResponseItems)
|
||||||
|
assert.Equal(t, responseHeaderMap, payload.ResponseMetadata.ResponseAttributes.Headers)
|
||||||
|
|
||||||
|
expectedRequestBody, _ := structpb.NewStruct(requestBody)
|
||||||
|
assert.Equal(t, expectedRequestBody, payload.Request)
|
||||||
|
expectedResponseBody, _ := structpb.NewStruct(response)
|
||||||
|
assert.Equal(t, expectedResponseBody, payload.Response)
|
||||||
|
expectedEventMetadata, _ := structpb.NewStruct(eventMetadata)
|
||||||
|
assert.Equal(t, expectedEventMetadata, payload.Metadata)
|
||||||
|
|
||||||
|
authenticationInfo := payload.AuthenticationInfo
|
||||||
|
assert.NotNil(t, authenticationInfo)
|
||||||
|
assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", authenticationInfo.PrincipalId)
|
||||||
|
assert.Equal(t, "Christian.Schaible@novatec-gmbh.de", *authenticationInfo.PrincipalEmail)
|
||||||
|
assert.Nil(t, authenticationInfo.ServiceAccountName)
|
||||||
|
assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
|
||||||
|
|
||||||
|
authorizationInfo := payload.AuthorizationInfo
|
||||||
|
assert.NotNil(t, authorizationInfo)
|
||||||
|
assert.Equal(t, 1, len(authorizationInfo))
|
||||||
|
assert.Equal(t, permission, *authorizationInfo[0].Permission)
|
||||||
|
assert.Equal(t, permissionGranted, *authorizationInfo[0].Granted)
|
||||||
|
assert.Equal(t, resourceName, authorizationInfo[0].Resource)
|
||||||
|
|
||||||
|
requestMetadata := payload.RequestMetadata
|
||||||
|
assert.NotNil(t, requestMetadata)
|
||||||
|
assert.Equal(t, clientIp, requestMetadata.CallerIp)
|
||||||
|
assert.Equal(t, userAgent, requestMetadata.CallerSuppliedUserAgent)
|
||||||
|
|
||||||
|
// Don't verify explicitly - trust on other unit test
|
||||||
|
assert.NotNil(t, userAgent, requestMetadata.RequestAttributes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_NewInsertId(t *testing.T) {
|
||||||
|
insertTime := time.Now().UTC()
|
||||||
|
location := "eu01"
|
||||||
|
workerId := uuid.NewString()
|
||||||
|
var eventSequenceNumber uint64 = 1
|
||||||
|
|
||||||
|
insertId := NewInsertId(insertTime, location, workerId, eventSequenceNumber)
|
||||||
|
expectedId := fmt.Sprintf("%d/%s/%s/%d", insertTime.UnixNano(), location, workerId, eventSequenceNumber)
|
||||||
|
assert.Equal(t, expectedId, insertId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_NewNewAuditRoutingIdentifier(t *testing.T) {
|
||||||
|
objectId := uuid.NewString()
|
||||||
|
objectType := pkgAuditCommon.ObjectTypeProject
|
||||||
|
|
||||||
|
routingIdentifier := NewAuditRoutingIdentifier(objectId, objectType)
|
||||||
|
assert.Equal(t, objectId, routingIdentifier.Identifier)
|
||||||
|
assert.Equal(t, objectType, routingIdentifier.Type)
|
||||||
|
}
|
||||||
184
internal/audit/api/schema_validation_test.go
Normal file
184
internal/audit/api/schema_validation_test.go
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"buf.build/go/protovalidate"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
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(pkgAuditCommon.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: operation_name: value is 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: operation_name: value does not match regex pattern `^stackit\\.[a-z0-9-]+\\.(?:v[0-9]+\\.)?(?:[a-z0-9-.]+\\.)?[a-z0-9-]+$`")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("visibility invalid", func(t *testing.T) {
|
||||||
|
event := newEvent()
|
||||||
|
event.Visibility = -1
|
||||||
|
|
||||||
|
err := validator.Validate(&event)
|
||||||
|
assert.EqualError(t, err, "validation error: visibility: value must be one of the defined enum values")
|
||||||
|
})
|
||||||
|
|
||||||
|
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: visibility: value is 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: object_identifier: value is 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: object_identifier.identifier: value is 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: object_identifier.identifier: value must be a valid 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: object_identifier.type: value is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("data nil", func(t *testing.T) {
|
||||||
|
event := newEvent()
|
||||||
|
event.Data = nil
|
||||||
|
|
||||||
|
err := validator.Validate(&event)
|
||||||
|
assert.EqualError(t, err, "validation error: data: exactly one field is required in oneof")
|
||||||
|
})
|
||||||
|
|
||||||
|
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: unencrypted_data.data: value is 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: unencrypted_data.protobuf_type: value is required")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_AuthenticationInfo(t *testing.T) {
|
||||||
|
|
||||||
|
validator, err := protovalidate.New()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
email := "x@x.x"
|
||||||
|
newEvent := func() auditV1.AuthenticationInfo {
|
||||||
|
return auditV1.AuthenticationInfo{
|
||||||
|
PrincipalId: "1234567890",
|
||||||
|
PrincipalEmail: &email,
|
||||||
|
ServiceAccountName: nil,
|
||||||
|
ServiceAccountDelegationInfo: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("valid event", func(t *testing.T) {
|
||||||
|
event := newEvent()
|
||||||
|
err := validator.Validate(&event)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid event without email", func(t *testing.T) {
|
||||||
|
event := newEvent()
|
||||||
|
event.PrincipalEmail = nil
|
||||||
|
err := validator.Validate(&event)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("principal id contains only whitespace", func(t *testing.T) {
|
||||||
|
event := newEvent()
|
||||||
|
event.PrincipalId = " "
|
||||||
|
err := validator.Validate(&event)
|
||||||
|
assert.EqualError(t, err, "validation error: principal_id: value does not match regex pattern `.*\\S.*`")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("principal email contains only whitespace", func(t *testing.T) {
|
||||||
|
event := newEvent()
|
||||||
|
whitespaceEmail := " "
|
||||||
|
event.PrincipalEmail = &whitespaceEmail
|
||||||
|
err := validator.Validate(&event)
|
||||||
|
assert.EqualError(t, err, "validation error: principal_email: value must be a valid email address")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing host in email", func(t *testing.T) {
|
||||||
|
event := newEvent()
|
||||||
|
invalidEmail := "@test.com"
|
||||||
|
event.PrincipalEmail = &invalidEmail
|
||||||
|
err := validator.Validate(&event)
|
||||||
|
assert.EqualError(t, err, "validation error: principal_email: value must be a valid email address")
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
460
internal/audit/api/test_data.go
Normal file
460
internal/audit/api/test_data.go
Normal file
|
|
@ -0,0 +1,460 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const clientCredentialsToken = "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOGJlZjc1LWRmY2QtNGE3My1hMzkxLTU0YTdhZjU3YTdkNiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic3RhY2tpdC1yZXNvdXJjZS1tYW5hZ2VyLWRldiJdLCJjbGllbnRfaWQiOiJzdGFja2l0LXJlc291cmNlLW1hbmFnZXItZGV2IiwiZXhwIjoxNzI0NDA1MzI2LCJpYXQiOjE3MjQ0MDQ0MjYsImlzcyI6Imh0dHBzOi8vYWNjb3VudHMuZGV2LnN0YWNraXQuY2xvdWQiLCJqdGkiOiJlNDZlYmEzOC1kZWRiLTQ1NDEtOTRmMy00OWY5N2E5MzRkNTgiLCJuYmYiOjE3MjQ0MDQ0MjYsInNjb3BlIjoidWFhLm5vbmUiLCJzdWIiOiJzdGFja2l0LXJlc291cmNlLW1hbmFnZXItZGV2In0.JP5Uy7AMdK4ukzQ6aOYzbVwEmq0Tp2ppQGRqGOhuVQgbqs6yJ33GKXo7RPsJVLw3FR7XAxENIVqNvzGotbDXr0NjBGdzyxIHzrOaUqM4w1iLzD1KF51dXFwkoigqDdD7Ze9eI_Uo3tSn8FwGLTSoO-ONQYpnceCiGut2Gc6VIL8HOLdh8dzlRENGQtgYd-3Y5zqpoLrsR2Bd-0sv15sF-5aI0CqcC8gE70JPImKf2u_IYI-TYMDNk86YSCtaYO5-alOrHXXWwgzSoH-r2s5qoOhPbei9myV_P4fdcKXxMqfap9hImXPUooVhpdUr1AabZw3MtW7rION8tJAiauhMQA"
|
||||||
|
const serviceAccountTokenUnderscoreSubject = "Bearer eyJraWQiOiJaVFJqWlRNek5tSmlNRGt3TldJMU5USTRZVGxpT1RjMllUWXlZVE16WldNIiwiYWxnIjoiUlM1MTIifQ.eyJzdWIiOiIxMGYzOGIwMV81MzRiXzQ3YmJfYTAzYV9lMjk0Y2EyYmU0ZGUiLCJhdWQiOlsic3RhY2tpdCIsImFwaSJdLCJzdGFja2l0L3NlcnZpY2VhY2NvdW50L3Rva2VuLnNvdXJjZSI6ImxlZ2FjeSIsInN0YWNraXQvc2VydmljZWFjY291bnQvbmFtZXNwYWNlIjoiYXBpIiwic3RhY2tpdC9wcm9qZWN0L3Byb2plY3QuaWQiOiJkYWNjNzgzMC04NDNlLTRjNWUtODZmZi1hYTBmYjUxZDYzNmYiLCJhenAiOiJjZDk0ZjAxYS1kZjJlLTQ0NTYtOTAyZS00OGY1ZTU3ZjBiNjMiLCJpc3MiOiJzdGFja2l0L3NlcnZpY2VhY2NvdW50Iiwic3RhY2tpdC9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiMTBmMzhiMDEtNTM0Yi00N2JiLWEwM2EtZTI5NGNhMmJlNGRlIiwiZXhwIjoxNzIyNjY5MzQzLCJpYXQiOjE3MjI1ODI5NDMsImVtYWlsIjoibXktc2VydmljZS15aWZjOWUxQHNhLnN0YWNraXQuY2xvdWQiLCJqdGkiOiI4NGMzMGE0Ni0xMDAxLTQzNmYtODU5Zi04OWMwYmExOWJlMWUifQ.bfD2TxfioqaKbqFJvnV_gq5zY_aoKVD2qzySMQjubaLQ5Vx_Tj95HU0q7gdNczNgcT0tBRyUp0pE4g4bwaPpB2MtYtUUunzpwG8sOX_OBchkorhcC4N50cdF5TR2pg0SMp3L6QBo3coHVbjHvaipshCj1NvyXYzARb4dSR0adrsIGnqy3IaScty1A2XQ7PN6SX_OVmxO5swpL0I-afKvCOffnChI3qmFAL5t6sFxm8PoaCWLIrkoxdtqxw5ZqsPPOJ0qDhssTuc3nE4JrQnzX8fZH5FiBVVHGT76KUNgPFd26UsVzbGqBXK20pn3pbIQHwbRiVOh6qanjr9kvHBXpQ"
|
||||||
|
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"
|
||||||
|
const userTokenWithSimpleAudience = "Bearer eyJhbGciOiJSUzUxMiIsImtpZCI6InNlcnZpY2UtYWNjb3VudC1mMDdiZjZhOC02MjA3LTRmOGItYjNlOS03M2VkMGJlYjg4ZjUiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL3N0YWNraXQtc2VydmljZS1hY2NvdW50LWRldi5hcHBzLjAxLmNmLmV1MDEuc3RhY2tpdC5jbG91ZCIsImVtYWlsIjoiTHVrYXMuU2NobWl0dEBzdGFja2l0LmNsb3VkIiwiZXhwIjoxNzMyMTgyMDM1LCJpYXQiOjE3MzIxNzg0MzUsImlzcyI6Imh0dHBzOi8vYXBpLmRldi5zdGFja2l0LmNsb3VkIiwianRpIjoiYzJiZTE2NTEtMWU1NC00ZTZlLWJhYzMtZWYwNzJiM2YwMTQ5IiwibmJmIjoxNzMyMTc4NDE4LCJyb2xlcyI6bnVsbCwic2NvcGUiOiJvcGVuaWQgZW1haWwgcG9ydGFsLWJmZiIsInN1YiI6IjVlNDI2YWVkLWM0ODctNGM0OC1hZjI1LTg3ZjY5Y2Y5Y2RkNCIsInVzZXJfaWQiOiIiLCJ4X2NsaWVudF9pZCI6IiIsInppZCI6IiJ9.notavailable"
|
||||||
|
|
||||||
|
var TestHeaders = map[string][]string{"user-agent": {"custom"}, "authorization": {userToken}}
|
||||||
|
var TestHeadersSa = map[string][]string{"user-agent": {"custom"}, "authorization": {serviceAccountTokenUnderscoreSubject}}
|
||||||
|
|
||||||
|
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"
|
||||||
|
email := "user@example.com"
|
||||||
|
auditEvent := &auditV1.AuditLogEntry{
|
||||||
|
LogName: fmt.Sprintf("%s/%s/logs/%s", pkgAuditCommon.ObjectTypeOrganization.Plural(), identifier, pkgAuditCommon.EventTypeAdminActivity),
|
||||||
|
ProtoPayload: &auditV1.AuditLog{
|
||||||
|
ServiceName: "resource-manager",
|
||||||
|
OperationName: "stackit.resourcemanager.v2.organization.created",
|
||||||
|
ResourceName: fmt.Sprintf("%s/%s", pkgAuditCommon.ObjectTypeOrganization.Plural(), identifier),
|
||||||
|
AuthenticationInfo: &auditV1.AuthenticationInfo{
|
||||||
|
PrincipalId: uuid.NewString(),
|
||||||
|
PrincipalEmail: &email,
|
||||||
|
ServiceAccountName: nil,
|
||||||
|
ServiceAccountDelegationInfo: nil,
|
||||||
|
},
|
||||||
|
AuthorizationInfo: []*auditV1.AuthorizationInfo{{
|
||||||
|
Resource: fmt.Sprintf("%s/%s", pkgAuditCommon.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,
|
||||||
|
}
|
||||||
|
|
||||||
|
objectIdentifier := &auditV1.ObjectIdentifier{
|
||||||
|
Identifier: identifier.String(),
|
||||||
|
Type: string(pkgAuditCommon.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"
|
||||||
|
email := "user@example.com"
|
||||||
|
auditEvent := &auditV1.AuditLogEntry{
|
||||||
|
LogName: fmt.Sprintf("%s/%s/logs/%s", pkgAuditCommon.ObjectTypeFolder.Plural(), identifier, pkgAuditCommon.EventTypeAdminActivity),
|
||||||
|
ProtoPayload: &auditV1.AuditLog{
|
||||||
|
ServiceName: "resource-manager",
|
||||||
|
OperationName: "stackit.resourcemanager.v2.folder.created",
|
||||||
|
ResourceName: fmt.Sprintf("%s/%s", pkgAuditCommon.ObjectTypeFolder.Plural(), identifier),
|
||||||
|
AuthenticationInfo: &auditV1.AuthenticationInfo{
|
||||||
|
PrincipalId: uuid.NewString(),
|
||||||
|
PrincipalEmail: &email,
|
||||||
|
ServiceAccountName: nil,
|
||||||
|
ServiceAccountDelegationInfo: nil,
|
||||||
|
},
|
||||||
|
AuthorizationInfo: []*auditV1.AuthorizationInfo{{
|
||||||
|
Resource: fmt.Sprintf("%s/%s", pkgAuditCommon.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,
|
||||||
|
}
|
||||||
|
|
||||||
|
objectIdentifier := &auditV1.ObjectIdentifier{
|
||||||
|
Identifier: identifier.String(),
|
||||||
|
Type: string(pkgAuditCommon.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"
|
||||||
|
email := "user@example.com"
|
||||||
|
auditEvent := &auditV1.AuditLogEntry{
|
||||||
|
LogName: fmt.Sprintf("%s/%s/logs/%s", pkgAuditCommon.ObjectTypeProject.Plural(), identifier, pkgAuditCommon.EventTypeAdminActivity),
|
||||||
|
ProtoPayload: &auditV1.AuditLog{
|
||||||
|
ServiceName: "resource-manager",
|
||||||
|
OperationName: "stackit.resourcemanager.v2.project.created",
|
||||||
|
ResourceName: fmt.Sprintf("%s/%s", pkgAuditCommon.ObjectTypeProject.Plural(), identifier),
|
||||||
|
AuthenticationInfo: &auditV1.AuthenticationInfo{
|
||||||
|
PrincipalId: uuid.NewString(),
|
||||||
|
PrincipalEmail: &email,
|
||||||
|
ServiceAccountName: nil,
|
||||||
|
ServiceAccountDelegationInfo: nil,
|
||||||
|
},
|
||||||
|
AuthorizationInfo: []*auditV1.AuthorizationInfo{{
|
||||||
|
Resource: fmt.Sprintf("%s/%s", pkgAuditCommon.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,
|
||||||
|
}
|
||||||
|
|
||||||
|
objectIdentifier := &auditV1.ObjectIdentifier{
|
||||||
|
Identifier: identifier.String(),
|
||||||
|
Type: string(pkgAuditCommon.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_{}}
|
||||||
|
email := "service-account@sa.stackit.cloud"
|
||||||
|
auditEvent := &auditV1.AuditLogEntry{
|
||||||
|
LogName: fmt.Sprintf("%s/%s/logs/%s", pkgAuditCommon.SystemIdentifier.Type, pkgAuditCommon.SystemIdentifier.Identifier, pkgAuditCommon.EventTypeSystemEvent),
|
||||||
|
ProtoPayload: &auditV1.AuditLog{
|
||||||
|
ServiceName: "resource-manager",
|
||||||
|
OperationName: "stackit.resourcemanager.v2.system.changed",
|
||||||
|
ResourceName: fmt.Sprintf("%s/%s", pkgAuditCommon.ObjectTypeProject.Plural(), identifier),
|
||||||
|
AuthenticationInfo: &auditV1.AuthenticationInfo{
|
||||||
|
PrincipalId: serviceAccountId,
|
||||||
|
PrincipalEmail: &email,
|
||||||
|
ServiceAccountName: &serviceAccountName,
|
||||||
|
ServiceAccountDelegationInfo: []*auditV1.ServiceAccountDelegationInfo{&delegationPrincipal},
|
||||||
|
},
|
||||||
|
AuthorizationInfo: []*auditV1.AuthorizationInfo{{
|
||||||
|
Resource: fmt.Sprintf("%s/%s", pkgAuditCommon.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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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_{}}
|
||||||
|
email := "service-account@sa.stackit.cloud"
|
||||||
|
auditEvent := &auditV1.AuditLogEntry{
|
||||||
|
LogName: fmt.Sprintf("%s/%s/logs/%s", pkgAuditCommon.ObjectTypeSystem.Plural(), identifier, pkgAuditCommon.EventTypeSystemEvent),
|
||||||
|
ProtoPayload: &auditV1.AuditLog{
|
||||||
|
ServiceName: "resource-manager",
|
||||||
|
OperationName: "stackit.resourcemanager.v2.system.changed",
|
||||||
|
ResourceName: fmt.Sprintf("%s/%s", pkgAuditCommon.ObjectTypeSystem.Plural(), identifier),
|
||||||
|
AuthenticationInfo: &auditV1.AuthenticationInfo{
|
||||||
|
PrincipalId: serviceAccountId,
|
||||||
|
PrincipalEmail: &email,
|
||||||
|
ServiceAccountName: &serviceAccountName,
|
||||||
|
ServiceAccountDelegationInfo: []*auditV1.ServiceAccountDelegationInfo{&delegationPrincipal},
|
||||||
|
},
|
||||||
|
AuthorizationInfo: []*auditV1.AuthorizationInfo{{
|
||||||
|
Resource: fmt.Sprintf("%s/%s", pkgAuditCommon.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,
|
||||||
|
}
|
||||||
|
|
||||||
|
if customization != nil {
|
||||||
|
(*customization)(auditEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return auditEvent
|
||||||
|
}
|
||||||
35
internal/audit/api/trace.go
Normal file
35
internal/audit/api/trace.go
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/propagation"
|
||||||
|
)
|
||||||
|
|
||||||
|
const traceParentHeader = "traceparent"
|
||||||
|
const traceStateHeader = "tracestate"
|
||||||
|
|
||||||
|
// TraceParentAndStateFromContext returns W3C conform trace parent and state from context
|
||||||
|
func TraceParentAndStateFromContext(ctx context.Context) (string, string) {
|
||||||
|
mapCarrier := propagation.MapCarrier{}
|
||||||
|
propagator := propagation.TraceContext{}
|
||||||
|
propagator.Inject(ctx, mapCarrier)
|
||||||
|
|
||||||
|
// Get trace parent from context w3c conform format
|
||||||
|
// Format: <version>-<trace-id>-<parent-id>-<trace-flags>
|
||||||
|
// Example: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
|
||||||
|
traceParent := mapCarrier[traceParentHeader]
|
||||||
|
traceState := mapCarrier[traceStateHeader]
|
||||||
|
|
||||||
|
return traceParent, traceState
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTraceParentAndStateToContext adds trace and state related information to the given context.
|
||||||
|
func AddTraceParentAndStateToContext(ctx context.Context, traceParent, traceState string) context.Context {
|
||||||
|
mapCarrier := propagation.MapCarrier{}
|
||||||
|
mapCarrier[traceParentHeader] = traceParent
|
||||||
|
mapCarrier[traceStateHeader] = traceState
|
||||||
|
|
||||||
|
propagator := propagation.TraceContext{}
|
||||||
|
return propagator.Extract(ctx, mapCarrier)
|
||||||
|
}
|
||||||
43
internal/audit/api/trace_test.go
Normal file
43
internal/audit/api/trace_test.go
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_AddTraceParentAndStateToContext(t *testing.T) {
|
||||||
|
expectedTraceParent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
|
||||||
|
expectedTraceState := "key1=value1,key2=value2"
|
||||||
|
ctx := AddTraceParentAndStateToContext(
|
||||||
|
context.Background(),
|
||||||
|
expectedTraceParent,
|
||||||
|
expectedTraceState)
|
||||||
|
|
||||||
|
span := trace.SpanFromContext(ctx)
|
||||||
|
|
||||||
|
assert.Equal(t, "00f067aa0ba902b7", span.SpanContext().SpanID().String())
|
||||||
|
assert.Equal(t, "4bf92f3577b34da6a3ce929d0e0e4736", span.SpanContext().TraceID().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_TraceParentAndStateFromContext(t *testing.T) {
|
||||||
|
tracer := otel.Tracer("test")
|
||||||
|
|
||||||
|
expectedTraceParent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
|
||||||
|
expectedTraceState := "key1=value1,key2=value2"
|
||||||
|
ctx := AddTraceParentAndStateToContext(
|
||||||
|
context.Background(),
|
||||||
|
expectedTraceParent,
|
||||||
|
expectedTraceState)
|
||||||
|
|
||||||
|
ctx, span := tracer.Start(ctx, "test")
|
||||||
|
traceParent, traceState := TraceParentAndStateFromContext(ctx)
|
||||||
|
|
||||||
|
assert.Equal(t, expectedTraceParent, traceParent)
|
||||||
|
assert.Equal(t, expectedTraceState, traceState)
|
||||||
|
assert.Equal(t, "00f067aa0ba902b7", span.SpanContext().SpanID().String())
|
||||||
|
assert.Equal(t, "4bf92f3577b34da6a3ce929d0e0e4736", span.SpanContext().TraceID().String())
|
||||||
|
}
|
||||||
237
internal/messaging/amqp_connection.go
Normal file
237
internal/messaging/amqp_connection.go
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
package messaging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Azure/go-amqp"
|
||||||
|
|
||||||
|
pkgCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const connectionTimeoutSeconds = 10
|
||||||
|
|
||||||
|
var ErrConnectionClosed = errors.New("amqp connection is closed")
|
||||||
|
|
||||||
|
type AmqpConnection struct {
|
||||||
|
ConnectionName string
|
||||||
|
Lock sync.RWMutex
|
||||||
|
BrokerUrl string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Conn AmqpConn
|
||||||
|
Dialer amqpDial
|
||||||
|
}
|
||||||
|
|
||||||
|
// AmqpConn is an abstraction of amqp.Conn
|
||||||
|
type AmqpConn interface {
|
||||||
|
NewSession(ctx context.Context, opts *amqp.SessionOptions) (AmqpSession, error)
|
||||||
|
Close() error
|
||||||
|
Done() <-chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type defaultAmqpConn struct {
|
||||||
|
Conn *amqp.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDefaultAmqpConn(conn *amqp.Conn) *defaultAmqpConn {
|
||||||
|
return &defaultAmqpConn{
|
||||||
|
Conn: conn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d defaultAmqpConn) NewSession(ctx context.Context, opts *amqp.SessionOptions) (AmqpSession, error) {
|
||||||
|
session, err := d.Conn.NewSession(ctx, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newDefaultAmqpSession(session), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d defaultAmqpConn) Close() error {
|
||||||
|
return d.Conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d defaultAmqpConn) Done() <-chan struct{} {
|
||||||
|
return d.Conn.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ AmqpConn = (*defaultAmqpConn)(nil)
|
||||||
|
|
||||||
|
type amqpDial interface {
|
||||||
|
Dial(ctx context.Context, addr string, opts *amqp.ConnOptions) (AmqpConn, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AmqpSession interface {
|
||||||
|
NewSender(ctx context.Context, target string, opts *amqp.SenderOptions) (AmqpSender, error)
|
||||||
|
Close(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type defaultAmqpSession struct {
|
||||||
|
Session *amqp.Session
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDefaultAmqpSession(session *amqp.Session) *defaultAmqpSession {
|
||||||
|
return &defaultAmqpSession{
|
||||||
|
Session: session,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *defaultAmqpSession) NewSender(ctx context.Context, target string, opts *amqp.SenderOptions) (AmqpSender, error) {
|
||||||
|
return s.Session.NewSender(ctx, target, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *defaultAmqpSession) Close(ctx context.Context) error {
|
||||||
|
return s.Session.Close(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ AmqpSession = (*defaultAmqpSession)(nil)
|
||||||
|
|
||||||
|
type defaultAmqpDialer struct{}
|
||||||
|
|
||||||
|
func (d *defaultAmqpDialer) Dial(ctx context.Context, addr string, opts *amqp.ConnOptions) (AmqpConn, error) {
|
||||||
|
dial, err := amqp.Dial(ctx, addr, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newDefaultAmqpConn(dial), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ amqpDial = (*defaultAmqpDialer)(nil)
|
||||||
|
|
||||||
|
func NewAmqpConnection(config pkgCommon.AmqpConnectionConfig, connectionName string) *AmqpConnection {
|
||||||
|
return &AmqpConnection{
|
||||||
|
ConnectionName: connectionName,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
BrokerUrl: config.BrokerUrl,
|
||||||
|
Username: config.Username,
|
||||||
|
Password: config.Password,
|
||||||
|
Dialer: &defaultAmqpDialer{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AmqpConnection) NewSender(ctx context.Context, topic string) (*AmqpSenderSession, error) {
|
||||||
|
if c.Conn == nil {
|
||||||
|
return nil, errors.New("connection is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.internalIsClosed() {
|
||||||
|
return nil, ErrConnectionClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Lock.RLock()
|
||||||
|
defer c.Lock.RUnlock()
|
||||||
|
|
||||||
|
// new session
|
||||||
|
newSession, err := c.Conn.NewSession(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("new session: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// new sender
|
||||||
|
newSender, err := newSession.NewSender(ctx, topic, nil)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("new internal sender: %w", err)
|
||||||
|
|
||||||
|
closeErr := newSession.Close(ctx)
|
||||||
|
if closeErr != nil {
|
||||||
|
return nil, errors.Join(err, fmt.Errorf("close session: %w", closeErr))
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AmqpSenderSession{newSession, newSender}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func As[T any](value any, err error) (*T, error) {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if value == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
castedValue, isType := value.(*T)
|
||||||
|
if !isType {
|
||||||
|
return nil, fmt.Errorf("could not cast value: %T", value)
|
||||||
|
}
|
||||||
|
return castedValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AmqpConnection) Connect() error {
|
||||||
|
c.Lock.Lock()
|
||||||
|
defer c.Lock.Unlock()
|
||||||
|
|
||||||
|
subCtx, cancel := context.WithTimeout(context.Background(), connectionTimeoutSeconds*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := c.internalConnect(subCtx); err != nil {
|
||||||
|
return fmt.Errorf("internal connect: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AmqpConnection) internalConnect(ctx context.Context) error {
|
||||||
|
if c.Conn == nil {
|
||||||
|
// Set credentials if specified
|
||||||
|
auth := amqp.SASLTypeAnonymous()
|
||||||
|
if c.Username != "" && c.Password != "" {
|
||||||
|
auth = amqp.SASLTypePlain(c.Username, c.Password)
|
||||||
|
} else {
|
||||||
|
slog.Debug("amqp connection: connect: using anonymous messaging")
|
||||||
|
}
|
||||||
|
options := &amqp.ConnOptions{
|
||||||
|
SASLType: auth,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize connection
|
||||||
|
conn, err := c.Dialer.Dial(ctx, c.BrokerUrl, options)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dial: %w", err)
|
||||||
|
}
|
||||||
|
c.Conn = conn
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AmqpConnection) Close() error {
|
||||||
|
c.Lock.Lock()
|
||||||
|
defer c.Lock.Unlock()
|
||||||
|
|
||||||
|
if err := c.internalClose(); err != nil {
|
||||||
|
return fmt.Errorf("internal close: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AmqpConnection) internalClose() error {
|
||||||
|
if c.Conn != nil {
|
||||||
|
if err := c.Conn.Close(); err != nil {
|
||||||
|
return fmt.Errorf("connection close: %w", err)
|
||||||
|
}
|
||||||
|
c.Conn = nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AmqpConnection) IsClosed() bool {
|
||||||
|
c.Lock.RLock()
|
||||||
|
defer c.Lock.RUnlock()
|
||||||
|
|
||||||
|
return c.internalIsClosed()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AmqpConnection) internalIsClosed() bool {
|
||||||
|
if c.Conn == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-c.Conn.Done():
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
231
internal/messaging/amqp_connection_pool.go
Normal file
231
internal/messaging/amqp_connection_pool.go
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
package messaging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
pkgCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type connectionProvider interface {
|
||||||
|
NewAmqpConnection(config pkgCommon.AmqpConnectionConfig, connectionName string) *AmqpConnection
|
||||||
|
}
|
||||||
|
|
||||||
|
type defaultAmqpConnectionProvider struct{}
|
||||||
|
|
||||||
|
func (p defaultAmqpConnectionProvider) NewAmqpConnection(config pkgCommon.AmqpConnectionConfig, connectionName string) *AmqpConnection {
|
||||||
|
return NewAmqpConnection(config, connectionName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ connectionProvider = (*defaultAmqpConnectionProvider)(nil)
|
||||||
|
|
||||||
|
type ConnectionPool interface {
|
||||||
|
Close() error
|
||||||
|
NewHandle() *ConnectionPoolHandle
|
||||||
|
GetConnection(handle *ConnectionPoolHandle) (*AmqpConnection, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AmqpConnectionPool struct {
|
||||||
|
Config pkgCommon.AmqpConnectionPoolConfig
|
||||||
|
ConnectionName string
|
||||||
|
Connections []*AmqpConnection
|
||||||
|
ConnectionProvider connectionProvider
|
||||||
|
HandleOffset int
|
||||||
|
Lock sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConnectionPoolHandle struct {
|
||||||
|
ConnectionOffset int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDefaultAmqpConnectionPool(config pkgCommon.AmqpConnectionConfig, connectionName string) (ConnectionPool, error) {
|
||||||
|
poolConfig := pkgCommon.AmqpConnectionPoolConfig{
|
||||||
|
Parameters: config,
|
||||||
|
PoolSize: 2,
|
||||||
|
}
|
||||||
|
return NewAmqpConnectionPool(poolConfig, connectionName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAmqpConnectionPool(config pkgCommon.AmqpConnectionPoolConfig, connectionName string) (ConnectionPool, error) {
|
||||||
|
if config.PoolSize == 0 {
|
||||||
|
config.PoolSize = 2
|
||||||
|
}
|
||||||
|
pool := &AmqpConnectionPool{
|
||||||
|
Config: config,
|
||||||
|
ConnectionName: connectionName,
|
||||||
|
Connections: make([]*AmqpConnection, 0),
|
||||||
|
ConnectionProvider: defaultAmqpConnectionProvider{},
|
||||||
|
HandleOffset: 0,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pool.initializeConnections(); err != nil {
|
||||||
|
if closeErr := pool.Close(); closeErr != nil {
|
||||||
|
return nil, errors.Join(err, fmt.Errorf("initialize amqp connection: pool closed: %w", closeErr))
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("initialize connections: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pool, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AmqpConnectionPool) initializeConnections() error {
|
||||||
|
if len(p.Connections) < p.Config.PoolSize {
|
||||||
|
p.Lock.Lock()
|
||||||
|
defer p.Lock.Unlock()
|
||||||
|
|
||||||
|
numMissingConnections := p.Config.PoolSize - len(p.Connections)
|
||||||
|
|
||||||
|
for i := 0; i < numMissingConnections; i++ {
|
||||||
|
if err := p.internalAddConnection(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AmqpConnectionPool) internalAddConnection() error {
|
||||||
|
newConnection, err := p.internalNewConnection()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("new connection: %w", err)
|
||||||
|
}
|
||||||
|
p.Connections = append(p.Connections, newConnection)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AmqpConnectionPool) internalNewConnection() (*AmqpConnection, error) {
|
||||||
|
conn := p.ConnectionProvider.NewAmqpConnection(p.Config.Parameters, p.ConnectionName)
|
||||||
|
if err := conn.Connect(); err != nil {
|
||||||
|
slog.Warn("amqp connection: failed to connect to amqp broker", slog.Any("err", err))
|
||||||
|
|
||||||
|
// retry
|
||||||
|
if err = conn.Connect(); err != nil {
|
||||||
|
connectErr := fmt.Errorf("new internal connection: %w", err)
|
||||||
|
if closeErr := conn.Close(); closeErr != nil {
|
||||||
|
// this case should never happen as the inner connection should always be null, therefore
|
||||||
|
// it should not have to be closed, i.e. be able to return errors.
|
||||||
|
return nil, errors.Join(connectErr, fmt.Errorf("close connection: %w", closeErr))
|
||||||
|
}
|
||||||
|
return nil, connectErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AmqpConnectionPool) Close() error {
|
||||||
|
p.Lock.Lock()
|
||||||
|
defer p.Lock.Unlock()
|
||||||
|
|
||||||
|
closeErrors := make([]error, 0)
|
||||||
|
for _, conn := range p.Connections {
|
||||||
|
if conn != nil {
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
closeErrors = append(closeErrors, fmt.Errorf("pooled connection: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.Connections = make([]*AmqpConnection, p.Config.PoolSize)
|
||||||
|
if len(closeErrors) > 0 {
|
||||||
|
return errors.Join(closeErrors...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AmqpConnectionPool) NewHandle() *ConnectionPoolHandle {
|
||||||
|
p.Lock.Lock()
|
||||||
|
defer p.Lock.Unlock()
|
||||||
|
|
||||||
|
offset := p.HandleOffset
|
||||||
|
p.HandleOffset++
|
||||||
|
|
||||||
|
offset %= p.Config.PoolSize
|
||||||
|
|
||||||
|
return &ConnectionPoolHandle{
|
||||||
|
ConnectionOffset: offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AmqpConnectionPool) GetConnection(handle *ConnectionPoolHandle) (*AmqpConnection, error) {
|
||||||
|
// get the requested connection or another one
|
||||||
|
conn, addConnection := p.nextConnectionForHandle(handle)
|
||||||
|
|
||||||
|
// renew the requested connection if the request connection is closed
|
||||||
|
if conn == nil || addConnection {
|
||||||
|
p.Lock.Lock()
|
||||||
|
|
||||||
|
// check that accessing the pool only with a valid index (out of bounds should only occur on shutdown)
|
||||||
|
connectionIndex := p.connectionIndex(handle, 0)
|
||||||
|
if p.Connections[connectionIndex] == nil {
|
||||||
|
connection, err := p.internalNewConnection()
|
||||||
|
if err != nil {
|
||||||
|
if conn == nil {
|
||||||
|
// case: connection could not be renewed and no connection to return has been found
|
||||||
|
p.Lock.Unlock()
|
||||||
|
return nil, fmt.Errorf("renew connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// case: connection could not be renewed but another connection will be returned
|
||||||
|
slog.Warn("amqp connection pool: get connection: renew connection: ", slog.Any("err", err))
|
||||||
|
} else {
|
||||||
|
// case: connection could be renewed and will be added to pool
|
||||||
|
p.Connections[connectionIndex] = connection
|
||||||
|
conn = connection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.Lock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if conn == nil {
|
||||||
|
return nil, fmt.Errorf("amqp connection pool: get connection: failed to obtain connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AmqpConnectionPool) nextConnectionForHandle(handle *ConnectionPoolHandle) (*AmqpConnection, bool) {
|
||||||
|
// retry as long as there are remaining connections in the pool
|
||||||
|
var conn *AmqpConnection
|
||||||
|
var addConnection bool
|
||||||
|
for i := 0; i < p.Config.PoolSize; i++ {
|
||||||
|
|
||||||
|
// get the next possible connection (considering the retry index)
|
||||||
|
idx := p.connectionIndex(handle, i)
|
||||||
|
p.Lock.RLock()
|
||||||
|
if idx < len(p.Connections) {
|
||||||
|
conn = p.Connections[idx]
|
||||||
|
} else {
|
||||||
|
// handle the edge case that the pool is empty on shutdown
|
||||||
|
conn = nil
|
||||||
|
}
|
||||||
|
p.Lock.RUnlock()
|
||||||
|
|
||||||
|
// remember that the requested is closed, retry with the next
|
||||||
|
if conn == nil {
|
||||||
|
addConnection = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the connection is closed, mark it by setting it to nil
|
||||||
|
if conn.IsClosed() {
|
||||||
|
p.Lock.Lock()
|
||||||
|
p.Connections[idx] = nil
|
||||||
|
p.Lock.Unlock()
|
||||||
|
|
||||||
|
addConnection = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, addConnection
|
||||||
|
}
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AmqpConnectionPool) connectionIndex(handle *ConnectionPoolHandle, iteration int) int {
|
||||||
|
if iteration+handle.ConnectionOffset >= p.Config.PoolSize {
|
||||||
|
return (iteration + handle.ConnectionOffset) % p.Config.PoolSize
|
||||||
|
}
|
||||||
|
return iteration + handle.ConnectionOffset
|
||||||
|
}
|
||||||
581
internal/messaging/amqp_connection_pool_test.go
Normal file
581
internal/messaging/amqp_connection_pool_test.go
Normal file
|
|
@ -0,0 +1,581 @@
|
||||||
|
package messaging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
|
pkgMessagingCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type connectionProviderMock struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *connectionProviderMock) NewAmqpConnection(config pkgMessagingCommon.AmqpConnectionConfig, connectionName string) *AmqpConnection {
|
||||||
|
args := p.Called(config, connectionName)
|
||||||
|
return args.Get(0).(*AmqpConnection)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ connectionProvider = (*connectionProviderMock)(nil)
|
||||||
|
|
||||||
|
func Test_AmqpConnectionPool_GetHandle(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("next handle", func(t *testing.T) {
|
||||||
|
pool := AmqpConnectionPool{
|
||||||
|
Config: pkgMessagingCommon.AmqpConnectionPoolConfig{PoolSize: 5},
|
||||||
|
HandleOffset: 0,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
handle := pool.NewHandle()
|
||||||
|
assert.NotNil(t, handle)
|
||||||
|
assert.Equal(t, 0, handle.ConnectionOffset)
|
||||||
|
assert.Equal(t, 1, pool.HandleOffset)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("next handle high offset", func(t *testing.T) {
|
||||||
|
pool := AmqpConnectionPool{
|
||||||
|
Config: pkgMessagingCommon.AmqpConnectionPoolConfig{PoolSize: 5},
|
||||||
|
HandleOffset: 13,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
handle := pool.NewHandle()
|
||||||
|
assert.NotNil(t, handle)
|
||||||
|
assert.Equal(t, 3, handle.ConnectionOffset)
|
||||||
|
assert.Equal(t, 14, pool.HandleOffset)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_AmqpConnectionPool_internalAddConnection(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("internal add connection", func(t *testing.T) {
|
||||||
|
conn := &amqpConnMock{}
|
||||||
|
|
||||||
|
dialer := &amqpDialMock{}
|
||||||
|
dialer.On("Dial", mock.Anything, mock.Anything, mock.Anything).Return(conn, nil)
|
||||||
|
|
||||||
|
connection := &AmqpConnection{
|
||||||
|
ConnectionName: "test",
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Dialer: dialer,
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionProvider := &connectionProviderMock{}
|
||||||
|
connectionProvider.On("NewAmqpConnection", mock.Anything, mock.Anything).Return(connection)
|
||||||
|
pool := AmqpConnectionPool{
|
||||||
|
Config: pkgMessagingCommon.AmqpConnectionPoolConfig{PoolSize: 5},
|
||||||
|
HandleOffset: 0,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
ConnectionProvider: connectionProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := pool.internalAddConnection()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 1, len(pool.Connections))
|
||||||
|
connectionProvider.AssertNumberOfCalls(t, "NewAmqpConnection", 1)
|
||||||
|
dialer.AssertNumberOfCalls(t, "Dial", 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("dialer error", func(t *testing.T) {
|
||||||
|
conn := &amqpConnMock{}
|
||||||
|
|
||||||
|
dialer := &amqpDialMock{}
|
||||||
|
var c *amqpConnMock = nil
|
||||||
|
dialer.On("Dial", mock.Anything, mock.Anything, mock.Anything).Return(c, errors.New("test error")).Once()
|
||||||
|
dialer.On("Dial", mock.Anything, mock.Anything, mock.Anything).Return(conn, nil)
|
||||||
|
|
||||||
|
connection := &AmqpConnection{
|
||||||
|
ConnectionName: "test",
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Dialer: dialer,
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionProvider := &connectionProviderMock{}
|
||||||
|
connectionProvider.On("NewAmqpConnection", mock.Anything, mock.Anything).Return(connection)
|
||||||
|
pool := AmqpConnectionPool{
|
||||||
|
Config: pkgMessagingCommon.AmqpConnectionPoolConfig{PoolSize: 5},
|
||||||
|
HandleOffset: 0,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
ConnectionProvider: connectionProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := pool.internalAddConnection()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 1, len(pool.Connections))
|
||||||
|
connectionProvider.AssertNumberOfCalls(t, "NewAmqpConnection", 1)
|
||||||
|
dialer.AssertNumberOfCalls(t, "Dial", 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("repetitive dialer error", func(t *testing.T) {
|
||||||
|
dialer := &amqpDialMock{}
|
||||||
|
var c *amqpConnMock = nil
|
||||||
|
dialer.On("Dial", mock.Anything, mock.Anything, mock.Anything).Return(c, errors.New("test error"))
|
||||||
|
|
||||||
|
connection := &AmqpConnection{
|
||||||
|
ConnectionName: "test",
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Dialer: dialer,
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionProvider := &connectionProviderMock{}
|
||||||
|
connectionProvider.On("NewAmqpConnection", mock.Anything, mock.Anything).Return(connection)
|
||||||
|
pool := AmqpConnectionPool{
|
||||||
|
Config: pkgMessagingCommon.AmqpConnectionPoolConfig{PoolSize: 5},
|
||||||
|
HandleOffset: 0,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
ConnectionProvider: connectionProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := pool.internalAddConnection()
|
||||||
|
assert.EqualError(t, err, "new connection: new internal connection: internal connect: dial: test error")
|
||||||
|
|
||||||
|
assert.Equal(t, 0, len(pool.Connections))
|
||||||
|
connectionProvider.AssertNumberOfCalls(t, "NewAmqpConnection", 1)
|
||||||
|
dialer.AssertNumberOfCalls(t, "Dial", 2)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_AmqpConnectionPool_initializeConnections(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("initialize connections successfully", func(t *testing.T) {
|
||||||
|
|
||||||
|
conn := &amqpConnMock{}
|
||||||
|
dialer := &amqpDialMock{}
|
||||||
|
dialer.On("Dial", mock.Anything, mock.Anything, mock.Anything).Return(conn, nil)
|
||||||
|
|
||||||
|
connection := &AmqpConnection{
|
||||||
|
ConnectionName: "test",
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Dialer: dialer,
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionProvider := &connectionProviderMock{}
|
||||||
|
connectionProvider.On("NewAmqpConnection", mock.Anything, mock.Anything).Return(connection)
|
||||||
|
pool := AmqpConnectionPool{
|
||||||
|
Config: pkgMessagingCommon.AmqpConnectionPoolConfig{PoolSize: 5},
|
||||||
|
HandleOffset: 0,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
ConnectionProvider: connectionProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := pool.initializeConnections()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 5, len(pool.Connections))
|
||||||
|
connectionProvider.AssertNumberOfCalls(t, "NewAmqpConnection", 5)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fail initialization of connections", func(t *testing.T) {
|
||||||
|
|
||||||
|
var c *amqpConnMock = nil
|
||||||
|
failingDialer := &amqpDialMock{}
|
||||||
|
failingDialer.On("Dial", mock.Anything, mock.Anything, mock.Anything).Return(c, errors.New("test error"))
|
||||||
|
|
||||||
|
failingConnection := &AmqpConnection{
|
||||||
|
ConnectionName: "test",
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Dialer: failingDialer,
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := &amqpConnMock{}
|
||||||
|
successfulDialer := &amqpDialMock{}
|
||||||
|
successfulDialer.On("Dial", mock.Anything, mock.Anything, mock.Anything).Return(conn, nil)
|
||||||
|
|
||||||
|
successfulConnection := &AmqpConnection{
|
||||||
|
ConnectionName: "test",
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Dialer: successfulDialer,
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionProvider := &connectionProviderMock{}
|
||||||
|
connectionProvider.On("NewAmqpConnection", mock.Anything, mock.Anything).Return(successfulConnection).Times(4)
|
||||||
|
connectionProvider.On("NewAmqpConnection", mock.Anything, mock.Anything).Return(failingConnection)
|
||||||
|
pool := AmqpConnectionPool{
|
||||||
|
Config: pkgMessagingCommon.AmqpConnectionPoolConfig{PoolSize: 5},
|
||||||
|
HandleOffset: 0,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
ConnectionProvider: connectionProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := pool.initializeConnections()
|
||||||
|
assert.EqualError(t, err, "new connection: new internal connection: internal connect: dial: test error")
|
||||||
|
|
||||||
|
assert.Equal(t, 4, len(pool.Connections))
|
||||||
|
connectionProvider.AssertNumberOfCalls(t, "NewAmqpConnection", 5)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_AmqpConnectionPool_Close(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("close connection successfully", func(t *testing.T) {
|
||||||
|
// add 5 connections to the pool
|
||||||
|
conn := &amqpConnMock{}
|
||||||
|
conn.On("Close").Return(nil)
|
||||||
|
|
||||||
|
dialer := &amqpDialMock{}
|
||||||
|
dialer.On("Dial", mock.Anything, mock.Anything, mock.Anything).Return(conn, nil)
|
||||||
|
|
||||||
|
connection := &AmqpConnection{
|
||||||
|
ConnectionName: "test",
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Dialer: dialer,
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionProvider := &connectionProviderMock{}
|
||||||
|
connectionProvider.On("NewAmqpConnection", mock.Anything, mock.Anything).Return(connection)
|
||||||
|
pool := AmqpConnectionPool{
|
||||||
|
Config: pkgMessagingCommon.AmqpConnectionPoolConfig{PoolSize: 5},
|
||||||
|
HandleOffset: 0,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
ConnectionProvider: connectionProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := pool.initializeConnections()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 5, len(pool.Connections))
|
||||||
|
|
||||||
|
// close the pool
|
||||||
|
err = pool.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 5, len(pool.Connections))
|
||||||
|
for _, c := range pool.Connections {
|
||||||
|
assert.Nil(t, c)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("close connection fail", func(t *testing.T) {
|
||||||
|
// add 5 connections to the pool
|
||||||
|
failingConn := &amqpConnMock{}
|
||||||
|
failingConn.On("Close").Return(errors.New("test error"))
|
||||||
|
|
||||||
|
failingDialer := &amqpDialMock{}
|
||||||
|
failingDialer.On("Dial", mock.Anything, mock.Anything, mock.Anything).Return(failingConn, nil)
|
||||||
|
|
||||||
|
failingConnection := &AmqpConnection{
|
||||||
|
ConnectionName: "test",
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Dialer: failingDialer,
|
||||||
|
}
|
||||||
|
|
||||||
|
successfulConn := &amqpConnMock{}
|
||||||
|
successfulConn.On("Close").Return(nil)
|
||||||
|
successfulDialer := &amqpDialMock{}
|
||||||
|
successfulDialer.On("Dial", mock.Anything, mock.Anything, mock.Anything).Return(successfulConn, nil)
|
||||||
|
|
||||||
|
successfulConnection := &AmqpConnection{
|
||||||
|
ConnectionName: "test",
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Dialer: successfulDialer,
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionProvider := &connectionProviderMock{}
|
||||||
|
connectionProvider.On("NewAmqpConnection", mock.Anything, mock.Anything).Return(successfulConnection).Times(2)
|
||||||
|
connectionProvider.On("NewAmqpConnection", mock.Anything, mock.Anything).Return(failingConnection).Times(2)
|
||||||
|
connectionProvider.On("NewAmqpConnection", mock.Anything, mock.Anything).Return(successfulConnection).Times(1)
|
||||||
|
|
||||||
|
pool := AmqpConnectionPool{
|
||||||
|
Config: pkgMessagingCommon.AmqpConnectionPoolConfig{PoolSize: 5},
|
||||||
|
HandleOffset: 0,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
ConnectionProvider: connectionProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := pool.initializeConnections()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 5, len(pool.Connections))
|
||||||
|
|
||||||
|
// close the pool
|
||||||
|
err = pool.Close()
|
||||||
|
assert.EqualError(t, err, "pooled connection: internal close: connection close: test error\npooled connection: internal close: connection close: test error")
|
||||||
|
assert.Equal(t, 5, len(pool.Connections))
|
||||||
|
for _, c := range pool.Connections {
|
||||||
|
assert.Nil(t, c)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_AmqpConnectionPool_nextConnectionForHandle(t *testing.T) {
|
||||||
|
channelReceiver := func(channel chan struct{}) <-chan struct{} {
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
|
||||||
|
newActiveConnection := func() *AmqpConnection {
|
||||||
|
channel := make(chan struct{})
|
||||||
|
conn := &amqpConnMock{}
|
||||||
|
conn.On("Done", mock.Anything).Return(channelReceiver(channel))
|
||||||
|
return &AmqpConnection{
|
||||||
|
ConnectionName: "test",
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Conn: conn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newClosedConnection := func() *AmqpConnection {
|
||||||
|
channel := make(chan struct{})
|
||||||
|
close(channel)
|
||||||
|
|
||||||
|
conn := &amqpConnMock{}
|
||||||
|
conn.On("Done", mock.Anything).Return(channelReceiver(channel))
|
||||||
|
return &AmqpConnection{
|
||||||
|
ConnectionName: "test",
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Conn: conn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("next connection for requested handle", func(t *testing.T) {
|
||||||
|
connections := make([]*AmqpConnection, 0)
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
connections = append(connections, newActiveConnection())
|
||||||
|
}
|
||||||
|
|
||||||
|
pool := AmqpConnectionPool{
|
||||||
|
Config: pkgMessagingCommon.AmqpConnectionPoolConfig{PoolSize: 5},
|
||||||
|
HandleOffset: 0,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Connections: connections,
|
||||||
|
}
|
||||||
|
|
||||||
|
connection, addConnection := pool.nextConnectionForHandle(&ConnectionPoolHandle{ConnectionOffset: 1})
|
||||||
|
assert.NotNil(t, connection)
|
||||||
|
assert.False(t, addConnection)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nil connection for requested handle", func(t *testing.T) {
|
||||||
|
connections := make([]*AmqpConnection, 0)
|
||||||
|
connections = append(connections, newActiveConnection())
|
||||||
|
connections = append(connections, nil)
|
||||||
|
connections = append(connections, nil)
|
||||||
|
connections = append(connections, newActiveConnection())
|
||||||
|
connections = append(connections, newActiveConnection())
|
||||||
|
|
||||||
|
pool := AmqpConnectionPool{
|
||||||
|
Config: pkgMessagingCommon.AmqpConnectionPoolConfig{PoolSize: 5},
|
||||||
|
HandleOffset: 0,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Connections: connections,
|
||||||
|
}
|
||||||
|
|
||||||
|
connection, addConnection := pool.nextConnectionForHandle(&ConnectionPoolHandle{ConnectionOffset: 1})
|
||||||
|
assert.NotNil(t, connection)
|
||||||
|
assert.True(t, addConnection)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("closed connection for requested handle", func(t *testing.T) {
|
||||||
|
connections := make([]*AmqpConnection, 0)
|
||||||
|
connections = append(connections, newActiveConnection())
|
||||||
|
connections = append(connections, newClosedConnection())
|
||||||
|
connections = append(connections, newClosedConnection())
|
||||||
|
connections = append(connections, newActiveConnection())
|
||||||
|
connections = append(connections, newActiveConnection())
|
||||||
|
|
||||||
|
pool := AmqpConnectionPool{
|
||||||
|
Config: pkgMessagingCommon.AmqpConnectionPoolConfig{PoolSize: 5},
|
||||||
|
HandleOffset: 0,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Connections: connections,
|
||||||
|
}
|
||||||
|
|
||||||
|
connection, addConnection := pool.nextConnectionForHandle(&ConnectionPoolHandle{ConnectionOffset: 1})
|
||||||
|
assert.NotNil(t, connection)
|
||||||
|
assert.True(t, addConnection)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no connection for requested handle", func(t *testing.T) {
|
||||||
|
connections := make([]*AmqpConnection, 0)
|
||||||
|
connections = append(connections, nil)
|
||||||
|
connections = append(connections, nil)
|
||||||
|
connections = append(connections, nil)
|
||||||
|
connections = append(connections, nil)
|
||||||
|
connections = append(connections, nil)
|
||||||
|
|
||||||
|
pool := AmqpConnectionPool{
|
||||||
|
Config: pkgMessagingCommon.AmqpConnectionPoolConfig{PoolSize: 5},
|
||||||
|
HandleOffset: 0,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Connections: connections,
|
||||||
|
}
|
||||||
|
|
||||||
|
connection, addConnection := pool.nextConnectionForHandle(&ConnectionPoolHandle{ConnectionOffset: 1})
|
||||||
|
assert.Nil(t, connection)
|
||||||
|
assert.True(t, addConnection)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("connection for requested handle with large index", func(t *testing.T) {
|
||||||
|
connections := make([]*AmqpConnection, 0)
|
||||||
|
connections = append(connections, nil)
|
||||||
|
connections = append(connections, nil)
|
||||||
|
connections = append(connections, nil)
|
||||||
|
connections = append(connections, newActiveConnection())
|
||||||
|
connections = append(connections, nil)
|
||||||
|
|
||||||
|
pool := AmqpConnectionPool{
|
||||||
|
Config: pkgMessagingCommon.AmqpConnectionPoolConfig{PoolSize: 5},
|
||||||
|
HandleOffset: 0,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Connections: connections,
|
||||||
|
}
|
||||||
|
|
||||||
|
connection, addConnection := pool.nextConnectionForHandle(&ConnectionPoolHandle{ConnectionOffset: 23})
|
||||||
|
assert.NotNil(t, connection)
|
||||||
|
assert.False(t, addConnection)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("connection for requested handle nil with large index", func(t *testing.T) {
|
||||||
|
connections := make([]*AmqpConnection, 0)
|
||||||
|
connections = append(connections, nil)
|
||||||
|
connections = append(connections, nil)
|
||||||
|
connections = append(connections, nil)
|
||||||
|
connections = append(connections, nil)
|
||||||
|
connections = append(connections, newActiveConnection())
|
||||||
|
|
||||||
|
pool := AmqpConnectionPool{
|
||||||
|
Config: pkgMessagingCommon.AmqpConnectionPoolConfig{PoolSize: 5},
|
||||||
|
HandleOffset: 0,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Connections: connections,
|
||||||
|
}
|
||||||
|
|
||||||
|
connection, addConnection := pool.nextConnectionForHandle(&ConnectionPoolHandle{ConnectionOffset: 23})
|
||||||
|
assert.NotNil(t, connection)
|
||||||
|
assert.True(t, addConnection)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_AmqpConnectionPool_GetConnection(t *testing.T) {
|
||||||
|
channelReceiver := func(channel chan struct{}) <-chan struct{} {
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
|
||||||
|
newActiveConnection := func() *AmqpConnection {
|
||||||
|
channel := make(chan struct{})
|
||||||
|
conn := &amqpConnMock{}
|
||||||
|
conn.On("Done", mock.Anything).Return(channelReceiver(channel))
|
||||||
|
return &AmqpConnection{
|
||||||
|
ConnectionName: "test",
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Conn: conn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("get connection for requested handle", func(t *testing.T) {
|
||||||
|
connections := make([]*AmqpConnection, 0)
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
connections = append(connections, newActiveConnection())
|
||||||
|
}
|
||||||
|
|
||||||
|
pool := AmqpConnectionPool{
|
||||||
|
Config: pkgMessagingCommon.AmqpConnectionPoolConfig{PoolSize: 5},
|
||||||
|
HandleOffset: 0,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Connections: connections,
|
||||||
|
}
|
||||||
|
|
||||||
|
connection, err := pool.GetConnection(&ConnectionPoolHandle{ConnectionOffset: 1})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, connection)
|
||||||
|
assert.Equal(t, connections[1], connection)
|
||||||
|
assert.Equal(t, 5, len(connections))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("add connection if missing", func(t *testing.T) {
|
||||||
|
connections := make([]*AmqpConnection, 5)
|
||||||
|
|
||||||
|
connectionProvider := &connectionProviderMock{}
|
||||||
|
connectionProvider.On("NewAmqpConnection", mock.Anything, mock.Anything).Return(newActiveConnection())
|
||||||
|
|
||||||
|
pool := AmqpConnectionPool{
|
||||||
|
Config: pkgMessagingCommon.AmqpConnectionPoolConfig{PoolSize: 5},
|
||||||
|
HandleOffset: 0,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Connections: connections,
|
||||||
|
ConnectionProvider: connectionProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
connection, err := pool.GetConnection(&ConnectionPoolHandle{ConnectionOffset: 1})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, connection)
|
||||||
|
assert.Equal(t, connections[1], connection)
|
||||||
|
assert.Equal(t, 5, len(connections))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("add connection fails returns alternative connection", func(t *testing.T) {
|
||||||
|
connections := make([]*AmqpConnection, 0)
|
||||||
|
connections = append(connections, newActiveConnection())
|
||||||
|
connections = append(connections, nil)
|
||||||
|
connections = append(connections, newActiveConnection())
|
||||||
|
connections = append(connections, newActiveConnection())
|
||||||
|
connections = append(connections, newActiveConnection())
|
||||||
|
|
||||||
|
connectionProvider := &connectionProviderMock{}
|
||||||
|
|
||||||
|
dialer := &amqpDialMock{}
|
||||||
|
var c *amqpConnMock = nil
|
||||||
|
dialer.On("Dial", mock.Anything, mock.Anything, mock.Anything).Return(c, fmt.Errorf("dial error"))
|
||||||
|
connection := &AmqpConnection{
|
||||||
|
ConnectionName: "test",
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Dialer: dialer,
|
||||||
|
}
|
||||||
|
connectionProvider.On("NewAmqpConnection", mock.Anything, mock.Anything).Return(connection)
|
||||||
|
|
||||||
|
pool := AmqpConnectionPool{
|
||||||
|
Config: pkgMessagingCommon.AmqpConnectionPoolConfig{PoolSize: 5},
|
||||||
|
HandleOffset: 0,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Connections: connections,
|
||||||
|
ConnectionProvider: connectionProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
connection, err := pool.GetConnection(&ConnectionPoolHandle{ConnectionOffset: 1})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, connection)
|
||||||
|
assert.Nil(t, connections[1])
|
||||||
|
assert.Equal(t, connections[2], connection)
|
||||||
|
assert.Equal(t, 5, len(connections))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("add connection fails", func(t *testing.T) {
|
||||||
|
connections := make([]*AmqpConnection, 0)
|
||||||
|
connections = append(connections, nil)
|
||||||
|
connections = append(connections, nil)
|
||||||
|
connections = append(connections, nil)
|
||||||
|
connections = append(connections, nil)
|
||||||
|
connections = append(connections, nil)
|
||||||
|
|
||||||
|
connectionProvider := &connectionProviderMock{}
|
||||||
|
|
||||||
|
dialer := &amqpDialMock{}
|
||||||
|
var c *amqpConnMock = nil
|
||||||
|
dialer.On("Dial", mock.Anything, mock.Anything, mock.Anything).Return(c, fmt.Errorf("dial error"))
|
||||||
|
connection := &AmqpConnection{
|
||||||
|
ConnectionName: "test",
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Dialer: dialer,
|
||||||
|
}
|
||||||
|
connectionProvider.On("NewAmqpConnection", mock.Anything, mock.Anything).Return(connection)
|
||||||
|
|
||||||
|
pool := AmqpConnectionPool{
|
||||||
|
Config: pkgMessagingCommon.AmqpConnectionPoolConfig{PoolSize: 5},
|
||||||
|
HandleOffset: 0,
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Connections: connections,
|
||||||
|
ConnectionProvider: connectionProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
connection, err := pool.GetConnection(&ConnectionPoolHandle{ConnectionOffset: 1})
|
||||||
|
assert.EqualError(t, err, "renew connection: new internal connection: internal connect: dial: dial error")
|
||||||
|
assert.Nil(t, connection)
|
||||||
|
assert.Equal(t, 5, len(connections))
|
||||||
|
})
|
||||||
|
}
|
||||||
309
internal/messaging/amqp_connection_test.go
Normal file
309
internal/messaging/amqp_connection_test.go
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
package messaging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Azure/go-amqp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
|
pkgCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type amqpConnMock struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *amqpConnMock) Done() <-chan struct{} {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(<-chan struct{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *amqpConnMock) NewSession(ctx context.Context, opts *amqp.SessionOptions) (AmqpSession, error) {
|
||||||
|
args := m.Called(ctx, opts)
|
||||||
|
return args.Get(0).(AmqpSession), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *amqpConnMock) Close() error {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ AmqpConn = (*amqpConnMock)(nil)
|
||||||
|
|
||||||
|
type amqpDialMock struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *amqpDialMock) Dial(ctx context.Context, addr string, opts *amqp.ConnOptions) (AmqpConn, error) {
|
||||||
|
args := m.Called(ctx, addr, opts)
|
||||||
|
return args.Get(0).(AmqpConn), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ amqpDial = (*amqpDialMock)(nil)
|
||||||
|
|
||||||
|
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)
|
||||||
|
return args.Get(0).(AmqpSender), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *amqpSessionMock) Close(ctx context.Context) error {
|
||||||
|
args := m.Called(ctx)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ AmqpSession = (*amqpSessionMock)(nil)
|
||||||
|
|
||||||
|
func Test_AmqpConnection_IsClosed(t *testing.T) {
|
||||||
|
connection := &AmqpConnection{
|
||||||
|
ConnectionName: "test",
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
channelReceiver := func(channel chan struct{}) <-chan struct{} {
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("is closed - connection nil", func(t *testing.T) {
|
||||||
|
assert.True(t, connection.IsClosed())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("is closed", func(t *testing.T) {
|
||||||
|
channel := make(chan struct{})
|
||||||
|
close(channel)
|
||||||
|
amqpConnMock := &amqpConnMock{}
|
||||||
|
amqpConnMock.On("Done").Return(channelReceiver(channel))
|
||||||
|
connection.Conn = amqpConnMock
|
||||||
|
|
||||||
|
assert.True(t, connection.IsClosed())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("is not closed", func(t *testing.T) {
|
||||||
|
channel := make(chan struct{})
|
||||||
|
amqpConnMock := &amqpConnMock{}
|
||||||
|
amqpConnMock.On("Done").Return(channelReceiver(channel))
|
||||||
|
connection.Conn = amqpConnMock
|
||||||
|
|
||||||
|
assert.False(t, connection.IsClosed())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_AmqpConnection_Close(t *testing.T) {
|
||||||
|
connection := &AmqpConnection{
|
||||||
|
ConnectionName: "test",
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("already closed", func(t *testing.T) {
|
||||||
|
assert.NoError(t, connection.Close())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("close error", func(t *testing.T) {
|
||||||
|
err := errors.New("test error")
|
||||||
|
|
||||||
|
amqpConnMock := &amqpConnMock{}
|
||||||
|
amqpConnMock.On("Close").Return(err)
|
||||||
|
connection.Conn = amqpConnMock
|
||||||
|
|
||||||
|
assert.EqualError(t, connection.Close(), "internal close: connection close: test error")
|
||||||
|
assert.NotNil(t, connection.Conn)
|
||||||
|
amqpConnMock.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("close without error", func(t *testing.T) {
|
||||||
|
amqpConnMock := &amqpConnMock{}
|
||||||
|
amqpConnMock.On("Close").Return(nil)
|
||||||
|
connection.Conn = amqpConnMock
|
||||||
|
|
||||||
|
assert.Nil(t, connection.Close())
|
||||||
|
assert.Nil(t, connection.Conn)
|
||||||
|
amqpConnMock.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_AmqpConnection_Connect(t *testing.T) {
|
||||||
|
connection := &AmqpConnection{
|
||||||
|
ConnectionName: "test",
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("already connected", func(t *testing.T) {
|
||||||
|
connection.Conn = &amqpConnMock{}
|
||||||
|
assert.NoError(t, connection.Connect())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("dial error", func(t *testing.T) {
|
||||||
|
connection.Conn = nil
|
||||||
|
connection.Username = "user"
|
||||||
|
connection.Password = "pass"
|
||||||
|
|
||||||
|
amqpDialMock := &amqpDialMock{}
|
||||||
|
var c *amqpConnMock = nil
|
||||||
|
amqpDialMock.On("Dial", mock.Anything, mock.Anything, mock.Anything).Return(c, errors.New("test error"))
|
||||||
|
connection.Dialer = amqpDialMock
|
||||||
|
|
||||||
|
assert.EqualError(t, connection.Connect(), "internal connect: dial: test error")
|
||||||
|
assert.Nil(t, connection.Conn)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("connect without error", func(t *testing.T) {
|
||||||
|
connection.Conn = nil
|
||||||
|
|
||||||
|
amqpDialMock := &amqpDialMock{}
|
||||||
|
amqpConn := &amqpConnMock{}
|
||||||
|
amqpDialMock.On("Dial", mock.Anything, mock.Anything, mock.Anything).Return(amqpConn, nil)
|
||||||
|
connection.Dialer = amqpDialMock
|
||||||
|
|
||||||
|
assert.NoError(t, connection.Connect())
|
||||||
|
assert.Equal(t, amqpConn, connection.Conn)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_AmqpConnection_NewSender(t *testing.T) {
|
||||||
|
connection := &AmqpConnection{
|
||||||
|
ConnectionName: "test",
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
channelReceiver := func(channel chan struct{}) <-chan struct{} {
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("connection not initialized", func(t *testing.T) {
|
||||||
|
sender, err := connection.NewSender(context.Background(), "topic")
|
||||||
|
assert.EqualError(t, err, "connection is not initialized")
|
||||||
|
assert.Nil(t, sender)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("connection is closed", func(t *testing.T) {
|
||||||
|
channel := make(chan struct{})
|
||||||
|
close(channel)
|
||||||
|
|
||||||
|
conn := &amqpConnMock{}
|
||||||
|
conn.On("Done").Return(channelReceiver(channel))
|
||||||
|
connection.Conn = conn
|
||||||
|
|
||||||
|
sender, err := connection.NewSender(context.Background(), "topic")
|
||||||
|
assert.EqualError(t, err, "amqp connection is closed")
|
||||||
|
assert.Nil(t, sender)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("session error", func(t *testing.T) {
|
||||||
|
channel := make(chan struct{})
|
||||||
|
|
||||||
|
var session *amqpSessionMock = nil
|
||||||
|
conn := &amqpConnMock{}
|
||||||
|
conn.On("NewSession", mock.Anything, mock.Anything).Return(session, errors.New("test error"))
|
||||||
|
conn.On("Done").Return(channelReceiver(channel))
|
||||||
|
connection.Conn = conn
|
||||||
|
|
||||||
|
sender, err := connection.NewSender(context.Background(), "topic")
|
||||||
|
assert.EqualError(t, err, "new session: test error")
|
||||||
|
assert.Nil(t, sender)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sender error", func(t *testing.T) {
|
||||||
|
channel := make(chan struct{})
|
||||||
|
|
||||||
|
sessionMock := &amqpSessionMock{}
|
||||||
|
var amqpSender *amqp.Sender = nil
|
||||||
|
sessionMock.On("NewSender", mock.Anything, mock.Anything, mock.Anything).Return(amqpSender, errors.New("test error"))
|
||||||
|
sessionMock.On("Close", mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
conn := &amqpConnMock{}
|
||||||
|
conn.On("Done").Return(channelReceiver(channel))
|
||||||
|
conn.On("NewSession", mock.Anything, mock.Anything).Return(sessionMock, nil)
|
||||||
|
connection.Conn = conn
|
||||||
|
|
||||||
|
sender, err := connection.NewSender(context.Background(), "topic")
|
||||||
|
assert.EqualError(t, err, "new internal sender: test error")
|
||||||
|
assert.Nil(t, sender)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("session close error", func(t *testing.T) {
|
||||||
|
channel := make(chan struct{})
|
||||||
|
|
||||||
|
sessionMock := &amqpSessionMock{}
|
||||||
|
var amqpSender *amqp.Sender = nil
|
||||||
|
sessionMock.On("NewSender", mock.Anything, mock.Anything, mock.Anything).Return(amqpSender, errors.New("test error"))
|
||||||
|
sessionMock.On("Close", mock.Anything).Return(errors.New("close error"))
|
||||||
|
|
||||||
|
conn := &amqpConnMock{}
|
||||||
|
conn.On("Done").Return(channelReceiver(channel))
|
||||||
|
conn.On("NewSession", mock.Anything, mock.Anything).Return(sessionMock, nil)
|
||||||
|
connection.Conn = conn
|
||||||
|
|
||||||
|
sender, err := connection.NewSender(context.Background(), "topic")
|
||||||
|
assert.EqualError(t, err, "new internal sender: test error\nclose session: close error")
|
||||||
|
assert.Nil(t, sender)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("get sender", func(t *testing.T) {
|
||||||
|
channel := make(chan struct{})
|
||||||
|
|
||||||
|
amqpSender := &amqp.Sender{}
|
||||||
|
sessionMock := &amqpSessionMock{}
|
||||||
|
sessionMock.On("NewSender", mock.Anything, mock.Anything, mock.Anything).Return(amqpSender, nil)
|
||||||
|
|
||||||
|
conn := &amqpConnMock{}
|
||||||
|
conn.On("Done").Return(channelReceiver(channel))
|
||||||
|
conn.On("NewSession", mock.Anything, mock.Anything).Return(sessionMock, nil)
|
||||||
|
connection.Conn = conn
|
||||||
|
|
||||||
|
sender, err := connection.NewSender(context.Background(), "topic")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, sender)
|
||||||
|
assert.Equal(t, amqpSender, sender.Sender)
|
||||||
|
assert.Equal(t, sessionMock, sender.Session)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_AmqpConnection_NewAmqpConnection(t *testing.T) {
|
||||||
|
config := pkgCommon.AmqpConnectionConfig{
|
||||||
|
BrokerUrl: "brokerUrl",
|
||||||
|
Username: "username",
|
||||||
|
Password: "password",
|
||||||
|
}
|
||||||
|
connection := NewAmqpConnection(config, "connectionName")
|
||||||
|
assert.NotNil(t, connection)
|
||||||
|
assert.Equal(t, connection.ConnectionName, "connectionName")
|
||||||
|
assert.Equal(t, connection.BrokerUrl, "brokerUrl")
|
||||||
|
assert.Equal(t, connection.Username, "username")
|
||||||
|
assert.Equal(t, connection.Password, "password")
|
||||||
|
assert.NotNil(t, connection.Dialer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_As(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("error", func(t *testing.T) {
|
||||||
|
value, err := As[amqp.Message](nil, errors.New("test error"))
|
||||||
|
assert.EqualError(t, err, "test error")
|
||||||
|
assert.Nil(t, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("value nil", func(t *testing.T) {
|
||||||
|
value, err := As[amqp.Message](nil, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("value not not type", func(t *testing.T) {
|
||||||
|
value, err := As[amqp.Message](struct{}{}, nil)
|
||||||
|
assert.EqualError(t, err, "could not cast value: struct {}")
|
||||||
|
assert.Nil(t, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("cast", func(t *testing.T) {
|
||||||
|
var sessionAny any = &amqpSessionMock{}
|
||||||
|
value, err := As[amqpSessionMock](sessionAny, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
81
internal/messaging/amqp_sender_session.go
Normal file
81
internal/messaging/amqp_sender_session.go
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
package messaging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Azure/go-amqp"
|
||||||
|
)
|
||||||
|
|
||||||
|
const amqpTopicPrefix = "topic://"
|
||||||
|
|
||||||
|
type AmqpSender interface {
|
||||||
|
Send(ctx context.Context, msg *amqp.Message, opts *amqp.SendOptions) error
|
||||||
|
Close(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type AmqpSenderSession struct {
|
||||||
|
Session AmqpSession
|
||||||
|
Sender AmqpSender
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AmqpSenderSession) Send(
|
||||||
|
topic string,
|
||||||
|
data [][]byte,
|
||||||
|
contentType string,
|
||||||
|
applicationProperties map[string]any,
|
||||||
|
) error {
|
||||||
|
// check topic name
|
||||||
|
if !strings.HasPrefix(topic, amqpTopicPrefix) {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"topic %q name lacks mandatory prefix %q",
|
||||||
|
topic,
|
||||||
|
amqpTopicPrefix,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if contentType == "" {
|
||||||
|
return errors.New("content-type is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare the amqp message
|
||||||
|
message := amqp.Message{
|
||||||
|
Header: &amqp.MessageHeader{
|
||||||
|
Durable: true,
|
||||||
|
},
|
||||||
|
Properties: &amqp.MessageProperties{
|
||||||
|
To: &topic,
|
||||||
|
ContentType: &contentType,
|
||||||
|
},
|
||||||
|
ApplicationProperties: applicationProperties,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
// send
|
||||||
|
ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancelFn()
|
||||||
|
return s.Sender.Send(ctx, &message, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AmqpSenderSession) Close() error {
|
||||||
|
ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancelFn()
|
||||||
|
|
||||||
|
var closeErrors []error
|
||||||
|
senderErr := s.Sender.Close(ctx)
|
||||||
|
if senderErr != nil {
|
||||||
|
closeErrors = append(closeErrors, senderErr)
|
||||||
|
}
|
||||||
|
sessionErr := s.Session.Close(ctx)
|
||||||
|
if sessionErr != nil {
|
||||||
|
closeErrors = append(closeErrors, sessionErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(closeErrors) > 0 {
|
||||||
|
return errors.Join(closeErrors...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
187
internal/messaging/amqp_sender_session_test.go
Normal file
187
internal/messaging/amqp_sender_session_test.go
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
package messaging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Azure/go-amqp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
type amqpSenderMock struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *amqpSenderMock) Send(ctx context.Context, msg *amqp.Message, opts *amqp.SendOptions) error {
|
||||||
|
return m.Called(ctx, msg, opts).Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *amqpSenderMock) Close(ctx context.Context) error {
|
||||||
|
return m.Called(ctx).Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ AmqpSender = (*amqpSenderMock)(nil)
|
||||||
|
|
||||||
|
func Test_AmqpSenderSession_Close(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("close without errors", func(t *testing.T) {
|
||||||
|
sender := &amqpSenderMock{}
|
||||||
|
sender.On("Close", mock.Anything).Return(nil)
|
||||||
|
session := &amqpSessionMock{}
|
||||||
|
session.On("Close", mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
senderSession := &AmqpSenderSession{
|
||||||
|
Sender: sender,
|
||||||
|
Session: session,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := senderSession.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
sender.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
session.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("close with sender error", func(t *testing.T) {
|
||||||
|
sender := &amqpSenderMock{}
|
||||||
|
sender.On("Close", mock.Anything).Return(errors.New("sender error"))
|
||||||
|
session := &amqpSessionMock{}
|
||||||
|
session.On("Close", mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
senderSession := &AmqpSenderSession{
|
||||||
|
Sender: sender,
|
||||||
|
Session: session,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := senderSession.Close()
|
||||||
|
assert.EqualError(t, err, "sender error")
|
||||||
|
|
||||||
|
sender.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
session.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("close with session error", func(t *testing.T) {
|
||||||
|
sender := &amqpSenderMock{}
|
||||||
|
sender.On("Close", mock.Anything).Return(nil)
|
||||||
|
session := &amqpSessionMock{}
|
||||||
|
session.On("Close", mock.Anything).Return(errors.New("session error"))
|
||||||
|
|
||||||
|
senderSession := &AmqpSenderSession{
|
||||||
|
Sender: sender,
|
||||||
|
Session: session,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := senderSession.Close()
|
||||||
|
assert.EqualError(t, err, "session error")
|
||||||
|
|
||||||
|
sender.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
session.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("close with sender and session error", func(t *testing.T) {
|
||||||
|
sender := &amqpSenderMock{}
|
||||||
|
sender.On("Close", mock.Anything).Return(errors.New("sender error"))
|
||||||
|
session := &amqpSessionMock{}
|
||||||
|
session.On("Close", mock.Anything).Return(errors.New("session error"))
|
||||||
|
|
||||||
|
senderSession := &AmqpSenderSession{
|
||||||
|
Sender: sender,
|
||||||
|
Session: session,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := senderSession.Close()
|
||||||
|
assert.EqualError(t, err, "sender error\nsession error")
|
||||||
|
|
||||||
|
sender.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
session.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_AmqpSenderSession_Send(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("invalid topic name", func(t *testing.T) {
|
||||||
|
sender := &amqpSenderMock{}
|
||||||
|
session := &amqpSessionMock{}
|
||||||
|
|
||||||
|
senderSession := &AmqpSenderSession{
|
||||||
|
Sender: sender,
|
||||||
|
Session: session,
|
||||||
|
}
|
||||||
|
|
||||||
|
data := [][]byte{[]byte("data")}
|
||||||
|
err := senderSession.Send("invalid", data, "application/json", map[string]interface{}{})
|
||||||
|
assert.EqualError(t, err, "topic \"invalid\" name lacks mandatory prefix \"topic://\"")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("content type missing", func(t *testing.T) {
|
||||||
|
sender := &amqpSenderMock{}
|
||||||
|
session := &amqpSessionMock{}
|
||||||
|
|
||||||
|
senderSession := &AmqpSenderSession{
|
||||||
|
Sender: sender,
|
||||||
|
Session: session,
|
||||||
|
}
|
||||||
|
|
||||||
|
data := [][]byte{[]byte("data")}
|
||||||
|
err := senderSession.Send("topic://some/name", data, "", map[string]interface{}{})
|
||||||
|
assert.EqualError(t, err, "content-type is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("send", func(t *testing.T) {
|
||||||
|
sender := &amqpSenderMock{}
|
||||||
|
sender.On("Send", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||||
|
session := &amqpSessionMock{}
|
||||||
|
|
||||||
|
senderSession := &AmqpSenderSession{
|
||||||
|
Sender: sender,
|
||||||
|
Session: session,
|
||||||
|
}
|
||||||
|
|
||||||
|
data := [][]byte{[]byte("data")}
|
||||||
|
applicationProperties := map[string]interface{}{}
|
||||||
|
applicationProperties["key"] = "value"
|
||||||
|
err := senderSession.Send("topic://some/name", data, "application/json", applicationProperties)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
sender.AssertNumberOfCalls(t, "Send", 1)
|
||||||
|
calls := sender.Calls
|
||||||
|
assert.Equal(t, 1, len(calls))
|
||||||
|
|
||||||
|
ctx, isCtx := calls[0].Arguments[0].(context.Context)
|
||||||
|
assert.True(t, isCtx)
|
||||||
|
assert.NotNil(t, ctx)
|
||||||
|
|
||||||
|
message, isMsg := calls[0].Arguments[1].(*amqp.Message)
|
||||||
|
assert.True(t, isMsg)
|
||||||
|
assert.True(t, message.Header.Durable)
|
||||||
|
assert.Equal(t, "topic://some/name", *message.Properties.To)
|
||||||
|
assert.Equal(t, "application/json", *message.Properties.ContentType)
|
||||||
|
assert.Equal(t, applicationProperties, message.ApplicationProperties)
|
||||||
|
assert.Equal(t, data, message.Data)
|
||||||
|
|
||||||
|
senderOptions, isSenderOptions := calls[0].Arguments[2].(*amqp.SendOptions)
|
||||||
|
assert.True(t, isSenderOptions)
|
||||||
|
assert.Nil(t, senderOptions)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("send fails", func(t *testing.T) {
|
||||||
|
sender := &amqpSenderMock{}
|
||||||
|
sender.On("Send", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("send fail"))
|
||||||
|
session := &amqpSessionMock{}
|
||||||
|
|
||||||
|
senderSession := &AmqpSenderSession{
|
||||||
|
Sender: sender,
|
||||||
|
Session: session,
|
||||||
|
}
|
||||||
|
|
||||||
|
data := [][]byte{[]byte("data")}
|
||||||
|
applicationProperties := map[string]interface{}{}
|
||||||
|
applicationProperties["key"] = "value"
|
||||||
|
|
||||||
|
err := senderSession.Send("topic://some/name", data, "application/json", applicationProperties)
|
||||||
|
assert.EqualError(t, err, "send fail")
|
||||||
|
sender.AssertNumberOfCalls(t, "Send", 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
24
internal/telemetry/telemetry.go
Normal file
24
internal/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
|
||||||
|
}
|
||||||
154
pkg/audit/api/api_legacy.go
Normal file
154
pkg/audit/api/api_legacy.go
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
internalAuditApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/internal/audit/api"
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
pkgMessagingApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DataTypeLegacyAuditEventV1 = "audit.v1.LegacyAuditEvent"
|
||||||
|
|
||||||
|
// StaticTopicNameConfig provides topic name information required for the topic name resolution.
|
||||||
|
type StaticTopicNameConfig 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 pkgMessagingApi.Api
|
||||||
|
topicNameResolver pkgAuditCommon.TopicNameResolver
|
||||||
|
tracer trace.Tracer
|
||||||
|
validator pkgAuditCommon.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 pkgMessagingApi.Api,
|
||||||
|
topicNameConfig StaticTopicNameConfig,
|
||||||
|
validator pkgAuditCommon.ProtobufValidator,
|
||||||
|
) (pkgAuditCommon.AuditApi, error) {
|
||||||
|
|
||||||
|
if messagingApi == nil {
|
||||||
|
return nil, pkgAuditCommon.ErrMessagingApiNil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Topic resolver
|
||||||
|
if topicNameConfig.TopicName == "" {
|
||||||
|
return nil, errors.New("topic name is required")
|
||||||
|
}
|
||||||
|
if !pkgAuditCommon.TopicNamePattern.MatchString(topicNameConfig.TopicName) {
|
||||||
|
return nil, fmt.Errorf("invalid topic name: %s - "+
|
||||||
|
"expected stackit-platform/t/swz/audit-log/{region}/{version}/{eventSource}/{additionalParts} "+
|
||||||
|
"where region is one of [conway, eu01, eu02, sx-stoi01], version is vX.Y, eventSource is the service name "+
|
||||||
|
"and additionalParts is a describing string the audit log relates to or 'events'", topicNameConfig.TopicName)
|
||||||
|
}
|
||||||
|
var topicNameResolver pkgAuditCommon.TopicNameResolver = &pkgAuditCommon.StaticTopicNameTestResolver{TopicName: topicNameConfig.TopicName}
|
||||||
|
|
||||||
|
// Audit api
|
||||||
|
var auditApi pkgAuditCommon.AuditApi = &LegacyAuditApi{
|
||||||
|
messagingApi: messagingApi,
|
||||||
|
topicNameResolver: topicNameResolver,
|
||||||
|
tracer: otel.Tracer("legacy-audit-api"),
|
||||||
|
validator: validator,
|
||||||
|
}
|
||||||
|
|
||||||
|
return auditApi, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log implements AuditApi.Log
|
||||||
|
func (a *LegacyAuditApi) Log(
|
||||||
|
ctx context.Context,
|
||||||
|
event *auditV1.AuditLogEntry,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
routableIdentifier *pkgAuditCommon.RoutableIdentifier,
|
||||||
|
) error {
|
||||||
|
|
||||||
|
cloudEvent, err := a.ValidateAndSerialize(ctx, event, visibility, routableIdentifier)
|
||||||
|
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(
|
||||||
|
ctx context.Context,
|
||||||
|
event *auditV1.AuditLogEntry,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
routableIdentifier *pkgAuditCommon.RoutableIdentifier,
|
||||||
|
) (*pkgAuditCommon.CloudEvent, error) {
|
||||||
|
|
||||||
|
ctx, span := a.tracer.Start(ctx, "validate-and-serialize")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
routableEvent, err := internalAuditApi.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(pkgAuditCommon.EventTypeDataAccess)) {
|
||||||
|
return nil, pkgAuditCommon.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 := internalAuditApi.ConvertAndSerializeIntoLegacyFormat(event, routableEvent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
traceParent, traceState := internalAuditApi.TraceParentAndStateFromContext(ctx)
|
||||||
|
|
||||||
|
message := pkgAuditCommon.CloudEvent{
|
||||||
|
SpecVersion: "1.0",
|
||||||
|
Source: event.ProtoPayload.ServiceName,
|
||||||
|
Id: event.InsertId,
|
||||||
|
Time: event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(),
|
||||||
|
DataContentType: pkgAuditCommon.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 *pkgAuditCommon.RoutableIdentifier,
|
||||||
|
cloudEvent *pkgAuditCommon.CloudEvent,
|
||||||
|
) error {
|
||||||
|
|
||||||
|
if cloudEvent != nil && cloudEvent.TraceParent != nil && cloudEvent.TraceState != nil {
|
||||||
|
ctx = internalAuditApi.AddTraceParentAndStateToContext(ctx, *cloudEvent.TraceParent, *cloudEvent.TraceState)
|
||||||
|
}
|
||||||
|
ctx, span := a.tracer.Start(ctx, "send")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
return internalAuditApi.Send(a.topicNameResolver, a.messagingApi, ctx, routableIdentifier, cloudEvent)
|
||||||
|
}
|
||||||
159
pkg/audit/api/api_legacy_dynamic.go
Normal file
159
pkg/audit/api/api_legacy_dynamic.go
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
internalAuditApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/internal/audit/api"
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
pkgMessagingApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 pkgMessagingApi.Api
|
||||||
|
tracer trace.Tracer
|
||||||
|
validator pkgAuditCommon.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 pkgMessagingApi.Api,
|
||||||
|
validator pkgAuditCommon.ProtobufValidator,
|
||||||
|
) (pkgAuditCommon.AuditApi, error) {
|
||||||
|
|
||||||
|
if messagingApi == nil {
|
||||||
|
return nil, pkgAuditCommon.ErrMessagingApiNil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audit api
|
||||||
|
var auditApi pkgAuditCommon.AuditApi = &DynamicLegacyAuditApi{
|
||||||
|
messagingApi: messagingApi,
|
||||||
|
tracer: otel.Tracer("dynamic-legacy-audit-api"),
|
||||||
|
validator: validator,
|
||||||
|
}
|
||||||
|
|
||||||
|
return auditApi, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log implements AuditApi.Log
|
||||||
|
func (a *DynamicLegacyAuditApi) Log(
|
||||||
|
ctx context.Context,
|
||||||
|
event *auditV1.AuditLogEntry,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
routableIdentifier *pkgAuditCommon.RoutableIdentifier,
|
||||||
|
) error {
|
||||||
|
|
||||||
|
cloudEvent, err := a.ValidateAndSerialize(ctx, event, visibility, routableIdentifier)
|
||||||
|
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(
|
||||||
|
ctx context.Context,
|
||||||
|
event *auditV1.AuditLogEntry,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
routableIdentifier *pkgAuditCommon.RoutableIdentifier,
|
||||||
|
) (*pkgAuditCommon.CloudEvent, error) {
|
||||||
|
|
||||||
|
ctx, span := a.tracer.Start(ctx, "validate-and-serialize")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
routableEvent, err := internalAuditApi.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(pkgAuditCommon.EventTypeDataAccess)) {
|
||||||
|
return nil, pkgAuditCommon.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 := internalAuditApi.ConvertAndSerializeIntoLegacyFormat(event, routableEvent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
traceParent, traceState := internalAuditApi.TraceParentAndStateFromContext(ctx)
|
||||||
|
|
||||||
|
message := pkgAuditCommon.CloudEvent{
|
||||||
|
SpecVersion: "1.0",
|
||||||
|
Source: event.ProtoPayload.ServiceName,
|
||||||
|
Id: event.InsertId,
|
||||||
|
Time: event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(),
|
||||||
|
DataContentType: pkgAuditCommon.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 *pkgAuditCommon.RoutableIdentifier,
|
||||||
|
cloudEvent *pkgAuditCommon.CloudEvent,
|
||||||
|
) error {
|
||||||
|
|
||||||
|
rawTopicName := ctx.Value(ContextKeyTopic)
|
||||||
|
if rawTopicName == nil {
|
||||||
|
return ErrNoTopicNameProvided
|
||||||
|
}
|
||||||
|
topicName := fmt.Sprintf("%s", rawTopicName)
|
||||||
|
if topicName == "" {
|
||||||
|
return ErrTopicNameEmpty
|
||||||
|
}
|
||||||
|
if !pkgAuditCommon.TopicNamePattern.MatchString(topicName) {
|
||||||
|
return fmt.Errorf("invalid topic name: %s - "+
|
||||||
|
"expected stackit-platform/t/swz/audit-log/{region}/{version}/{eventSource}/{additionalParts} "+
|
||||||
|
"where region is one of [conway, eu01, eu02, sx-stoi01], version is vX.Y, eventSource is the service name "+
|
||||||
|
"and additionalParts is a describing string the audit log relates to or 'events'", topicName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var topicNameResolver pkgAuditCommon.TopicNameResolver = &pkgAuditCommon.StaticTopicNameTestResolver{TopicName: topicName}
|
||||||
|
|
||||||
|
if cloudEvent != nil && cloudEvent.TraceParent != nil && cloudEvent.TraceState != nil {
|
||||||
|
ctx = internalAuditApi.AddTraceParentAndStateToContext(ctx, *cloudEvent.TraceParent, *cloudEvent.TraceState)
|
||||||
|
}
|
||||||
|
ctx, span := a.tracer.Start(ctx, "send")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
return internalAuditApi.Send(topicNameResolver, a.messagingApi, ctx, routableIdentifier, cloudEvent)
|
||||||
|
}
|
||||||
524
pkg/audit/api/api_legacy_dynamic_test.go
Normal file
524
pkg/audit/api/api_legacy_dynamic_test.go
Normal file
|
|
@ -0,0 +1,524 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"buf.build/go/protovalidate"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
internalAuditApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/internal/audit/api"
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
pkgMessagingApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/api"
|
||||||
|
pkgMessagingCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/common"
|
||||||
|
pkgMessagingTest "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 := pkgMessagingTest.NewSolaceContainer(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer solaceContainer.Stop()
|
||||||
|
|
||||||
|
// Instantiate the messaging api
|
||||||
|
amqpApi, err := pkgMessagingApi.NewAmqpApi(
|
||||||
|
pkgMessagingCommon.AmqpConnectionPoolConfig{
|
||||||
|
Parameters: pkgMessagingCommon.AmqpConnectionConfig{BrokerUrl: solaceContainer.AmqpConnectionString},
|
||||||
|
PoolSize: 1,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Validator
|
||||||
|
validator, err := protovalidate.New()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
topicSubscriptionTopicPattern := "stackit-platform/t/swz/audit-log/>"
|
||||||
|
|
||||||
|
// 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://stackit-platform/t/swz/audit-log/eu01/v1/resource-manager/organization-rejected"
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
// Instantiate audit api
|
||||||
|
auditApi, err := NewDynamicLegacyAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event, objectIdentifier := internalAuditApi.NewOrganizationAuditEvent(nil)
|
||||||
|
event.LogName = strings.Replace(event.LogName, string(pkgAuditCommon.EventTypeAdminActivity), string(pkgAuditCommon.EventTypeDataAccess), 1)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||||
|
ctxWithTopic := context.WithValue(ctx, ContextKeyTopic, topicName)
|
||||||
|
assert.ErrorIs(t, auditApi.Log(
|
||||||
|
ctxWithTopic,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier),
|
||||||
|
), pkgAuditCommon.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://stackit-platform/t/swz/audit-log/eu01/v1/resource-manager/organization-created"
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
// Instantiate audit api
|
||||||
|
auditApi, err := NewDynamicLegacyAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event, objectIdentifier := internalAuditApi.NewOrganizationAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||||
|
ctxWithTopic := context.WithValue(ctx, ContextKeyTopic, topicName)
|
||||||
|
assert.NoError(t, auditApi.Log(
|
||||||
|
ctxWithTopic,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier),
|
||||||
|
))
|
||||||
|
|
||||||
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
validateSentMessage(t, topicName, message, event)
|
||||||
|
})
|
||||||
|
|
||||||
|
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://stackit-platform/t/swz/audit-log/eu01/v1/resource-manager/organization-created"
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
// Instantiate audit api
|
||||||
|
auditApi, err := NewDynamicLegacyAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event, objectIdentifier := internalAuditApi.NewOrganizationAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||||
|
ctxWithTopic := context.WithValue(ctx, ContextKeyTopic, topicName)
|
||||||
|
assert.NoError(t, auditApi.Log(
|
||||||
|
ctxWithTopic,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier),
|
||||||
|
))
|
||||||
|
|
||||||
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
validateSentMessage(t, topicName, message, event)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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://stackit-platform/t/swz/audit-log/eu01/v1/resource-manager/folder-created"
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
// Instantiate audit api
|
||||||
|
auditApi, err := NewDynamicLegacyAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event, objectIdentifier := internalAuditApi.NewFolderAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||||
|
ctxWithTopic := context.WithValue(ctx, ContextKeyTopic, topicName)
|
||||||
|
assert.NoError(t, auditApi.Log(
|
||||||
|
ctxWithTopic,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier),
|
||||||
|
))
|
||||||
|
|
||||||
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
validateSentMessage(t, topicName, message, event)
|
||||||
|
})
|
||||||
|
|
||||||
|
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://stackit-platform/t/swz/audit-log/eu01/v1/resource-manager/folder-created"
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
// Instantiate audit api
|
||||||
|
auditApi, err := NewDynamicLegacyAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event, objectIdentifier := internalAuditApi.NewFolderAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||||
|
ctxWithTopic := context.WithValue(ctx, ContextKeyTopic, topicName)
|
||||||
|
assert.NoError(t, auditApi.Log(
|
||||||
|
ctxWithTopic,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier),
|
||||||
|
))
|
||||||
|
|
||||||
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
validateSentMessage(t, topicName, message, event)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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://stackit-platform/t/swz/audit-log/eu01/v1/resource-manager/project-created"
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
// Instantiate audit api
|
||||||
|
auditApi, err := NewDynamicLegacyAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event, objectIdentifier := internalAuditApi.NewProjectAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||||
|
ctxWithTopic := context.WithValue(ctx, ContextKeyTopic, topicName)
|
||||||
|
assert.NoError(t, auditApi.Log(
|
||||||
|
ctxWithTopic,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier),
|
||||||
|
))
|
||||||
|
|
||||||
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
validateSentMessage(t, topicName, message, event)
|
||||||
|
})
|
||||||
|
|
||||||
|
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://stackit-platform/t/swz/audit-log/eu01/v1/resource-manager/project-created"
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
// Instantiate audit api
|
||||||
|
auditApi, err := NewDynamicLegacyAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event, objectIdentifier := internalAuditApi.NewProjectAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||||
|
ctxWithTopic := context.WithValue(ctx, ContextKeyTopic, topicName)
|
||||||
|
assert.NoError(t, auditApi.Log(
|
||||||
|
ctxWithTopic,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier),
|
||||||
|
))
|
||||||
|
|
||||||
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
validateSentMessage(t, topicName, message, event)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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://stackit-platform/t/swz/audit-log/eu01/v1/resource-manager/project-system-changed"
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
// Instantiate audit api
|
||||||
|
auditApi, err := NewDynamicLegacyAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event := internalAuditApi.NewProjectSystemAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||||
|
ctxWithTopic := context.WithValue(ctx, ContextKeyTopic, topicName)
|
||||||
|
assert.NoError(t,
|
||||||
|
auditApi.Log(
|
||||||
|
ctxWithTopic,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.RoutableSystemIdentifier,
|
||||||
|
))
|
||||||
|
|
||||||
|
// 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.Equal(t, "", message.ApplicationProperties["cloudEvents:traceparent"])
|
||||||
|
assert.Equal(t, "", message.ApplicationProperties["cloudEvents:tracestate"])
|
||||||
|
|
||||||
|
// Check deserialized message
|
||||||
|
var auditEvent internalAuditApi.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://stackit-platform/t/swz/audit-log/eu01/v1/resource-manager/system-changed"
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
// Instantiate audit api
|
||||||
|
auditApi, err := NewDynamicLegacyAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event := internalAuditApi.NewSystemAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||||
|
ctxWithTopic := context.WithValue(ctx, ContextKeyTopic, topicName)
|
||||||
|
assert.NoError(t,
|
||||||
|
auditApi.Log(
|
||||||
|
ctxWithTopic,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.RoutableSystemIdentifier,
|
||||||
|
))
|
||||||
|
|
||||||
|
// 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.Equal(t, "", message.ApplicationProperties["cloudEvents:traceparent"])
|
||||||
|
assert.Equal(t, "", message.ApplicationProperties["cloudEvents:tracestate"])
|
||||||
|
|
||||||
|
// Check deserialized message
|
||||||
|
var auditEvent internalAuditApi.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://stackit-platform/t/swz/audit-log/eu01/v1/resource-manager/organization-created"
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
// Instantiate audit api
|
||||||
|
auditApi, err := NewDynamicLegacyAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event, objectIdentifier := internalAuditApi.NewOrganizationAuditEvent(nil)
|
||||||
|
escapedQuery := url.QueryEscape("param=value")
|
||||||
|
event.ProtoPayload.RequestMetadata.RequestAttributes.Query = &escapedQuery
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||||
|
ctxWithTopic := context.WithValue(ctx, ContextKeyTopic, topicName)
|
||||||
|
assert.NoError(t, auditApi.Log(
|
||||||
|
ctxWithTopic,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier),
|
||||||
|
))
|
||||||
|
|
||||||
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
validateSentMessageWithDetails(t, topicName, message, event)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 pkgAuditCommon.ProtobufValidator = validator
|
||||||
|
|
||||||
|
auditApi := DynamicLegacyAuditApi{
|
||||||
|
tracer: otel.Tracer("test"),
|
||||||
|
validator: protobufValidator,
|
||||||
|
}
|
||||||
|
|
||||||
|
event := internalAuditApi.NewSystemAuditEvent(nil)
|
||||||
|
_, err := auditApi.ValidateAndSerialize(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.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 pkgAuditCommon.ProtobufValidator = validator
|
||||||
|
|
||||||
|
auditApi := DynamicLegacyAuditApi{
|
||||||
|
tracer: otel.Tracer("test"),
|
||||||
|
validator: protobufValidator,
|
||||||
|
}
|
||||||
|
|
||||||
|
event := internalAuditApi.NewSystemAuditEvent(nil)
|
||||||
|
err := auditApi.Log(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.RoutableSystemIdentifier)
|
||||||
|
assert.ErrorIs(t, err, expectedError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDynamicLegacyAuditApi_Log_NilEvent(t *testing.T) {
|
||||||
|
auditApi := DynamicLegacyAuditApi{tracer: otel.Tracer("test")}
|
||||||
|
err := auditApi.Log(context.Background(), nil, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.RoutableSystemIdentifier)
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.ErrEventNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDynamicLegacyAuditApi_ConvertAndSerializeIntoLegacyFormatInvalidObjectIdentifierType(t *testing.T) {
|
||||||
|
customization := func(event *auditV1.AuditLogEntry,
|
||||||
|
objectIdentifier *auditV1.ObjectIdentifier) {
|
||||||
|
objectIdentifier.Type = "invalid"
|
||||||
|
}
|
||||||
|
event, objectIdentifier := internalAuditApi.NewProjectAuditEvent(&customization)
|
||||||
|
|
||||||
|
validator := &ProtobufValidatorMock{}
|
||||||
|
validator.On("Validate", mock.Anything).Return(nil)
|
||||||
|
var protobufValidator pkgAuditCommon.ProtobufValidator = validator
|
||||||
|
|
||||||
|
auditApi := DynamicLegacyAuditApi{
|
||||||
|
tracer: otel.Tracer("test"),
|
||||||
|
validator: protobufValidator,
|
||||||
|
}
|
||||||
|
_, err := auditApi.ValidateAndSerialize(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.NewRoutableIdentifier(objectIdentifier))
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.ErrUnsupportedRoutableType)
|
||||||
|
}
|
||||||
665
pkg/audit/api/api_legacy_test.go
Normal file
665
pkg/audit/api/api_legacy_test.go
Normal file
|
|
@ -0,0 +1,665 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"buf.build/go/protovalidate"
|
||||||
|
"github.com/Azure/go-amqp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
internalAuditApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/internal/audit/api"
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
pkgMessagingApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/api"
|
||||||
|
pkgMessagingCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/common"
|
||||||
|
pkgMessagingTest "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 := pkgMessagingTest.NewSolaceContainer(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer solaceContainer.Stop()
|
||||||
|
|
||||||
|
// Instantiate the messaging api
|
||||||
|
amqpApi, err := pkgMessagingApi.NewAmqpApi(pkgMessagingCommon.AmqpConnectionPoolConfig{
|
||||||
|
Parameters: pkgMessagingCommon.AmqpConnectionConfig{BrokerUrl: solaceContainer.AmqpConnectionString},
|
||||||
|
PoolSize: 1,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Validator
|
||||||
|
validator, err := protovalidate.New()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
topicSubscriptionTopicPattern := "stackit-platform/t/swz/audit-log/>"
|
||||||
|
|
||||||
|
// 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://stackit-platform/t/swz/audit-log/eu01/v1/resource-manager/organization-rejected"
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
// Instantiate audit api
|
||||||
|
auditApi, err := NewLegacyAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
StaticTopicNameConfig{TopicName: topicName},
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event, objectIdentifier := internalAuditApi.NewOrganizationAuditEvent(nil)
|
||||||
|
event.LogName = strings.Replace(event.LogName, string(pkgAuditCommon.EventTypeAdminActivity), string(pkgAuditCommon.EventTypeDataAccess), 1)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
assert.ErrorIs(t, auditApi.Log(
|
||||||
|
ctx,
|
||||||
|
event,
|
||||||
|
auditV1.Visibility_VISIBILITY_PUBLIC,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier),
|
||||||
|
), pkgAuditCommon.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://stackit-platform/t/swz/audit-log/eu01/v1/resource-manager/organization-created"
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
// Instantiate audit api
|
||||||
|
auditApi, err := NewLegacyAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
StaticTopicNameConfig{TopicName: topicName},
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event, objectIdentifier := internalAuditApi.NewOrganizationAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||||
|
assert.NoError(t, auditApi.Log(
|
||||||
|
ctx,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier),
|
||||||
|
))
|
||||||
|
|
||||||
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
validateSentMessage(t, topicName, message, event)
|
||||||
|
})
|
||||||
|
|
||||||
|
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://stackit-platform/t/swz/audit-log/eu01/v1/resource-manager/organization-created"
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
// Instantiate audit api
|
||||||
|
auditApi, err := NewLegacyAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
StaticTopicNameConfig{TopicName: topicName},
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event, objectIdentifier := internalAuditApi.NewOrganizationAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||||
|
assert.NoError(t, auditApi.Log(
|
||||||
|
ctx,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier),
|
||||||
|
))
|
||||||
|
|
||||||
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
validateSentMessage(t, topicName, message, event)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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://stackit-platform/t/swz/audit-log/eu01/v1/resource-manager/folder-created"
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
// Instantiate audit api
|
||||||
|
auditApi, err := NewLegacyAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
StaticTopicNameConfig{TopicName: topicName},
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event, objectIdentifier := internalAuditApi.NewFolderAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||||
|
assert.NoError(t, auditApi.Log(
|
||||||
|
ctx,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier),
|
||||||
|
))
|
||||||
|
|
||||||
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
validateSentMessage(t, topicName, message, event)
|
||||||
|
})
|
||||||
|
|
||||||
|
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://stackit-platform/t/swz/audit-log/eu01/v1/resource-manager/folder-created"
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
// Instantiate audit api
|
||||||
|
auditApi, err := NewLegacyAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
StaticTopicNameConfig{TopicName: topicName},
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event, objectIdentifier := internalAuditApi.NewFolderAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||||
|
assert.NoError(t, auditApi.Log(
|
||||||
|
ctx,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier),
|
||||||
|
))
|
||||||
|
|
||||||
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
validateSentMessage(t, topicName, message, event)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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://stackit-platform/t/swz/audit-log/eu01/v1/resource-manager/project-created"
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
// Instantiate audit api
|
||||||
|
auditApi, err := NewLegacyAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
StaticTopicNameConfig{TopicName: topicName},
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event, objectIdentifier := internalAuditApi.NewProjectAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||||
|
assert.NoError(t, auditApi.Log(
|
||||||
|
ctx,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier),
|
||||||
|
))
|
||||||
|
|
||||||
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
validateSentMessage(t, topicName, message, event)
|
||||||
|
})
|
||||||
|
|
||||||
|
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://stackit-platform/t/swz/audit-log/eu01/v1/resource-manager/project-created"
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
// Instantiate audit api
|
||||||
|
auditApi, err := NewLegacyAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
StaticTopicNameConfig{TopicName: topicName},
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event, objectIdentifier := internalAuditApi.NewProjectAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||||
|
assert.NoError(t, auditApi.Log(
|
||||||
|
ctx,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier),
|
||||||
|
))
|
||||||
|
|
||||||
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
validateSentMessage(t, topicName, message, event)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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://stackit-platform/t/swz/audit-log/eu01/v1/resource-manager/project-system-changed"
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
// Instantiate audit api
|
||||||
|
auditApi, err := NewLegacyAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
StaticTopicNameConfig{TopicName: topicName},
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event := internalAuditApi.NewProjectSystemAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||||
|
assert.NoError(t,
|
||||||
|
auditApi.Log(
|
||||||
|
ctx,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.RoutableSystemIdentifier,
|
||||||
|
))
|
||||||
|
|
||||||
|
// 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.Equal(t, "", message.ApplicationProperties["cloudEvents:traceparent"])
|
||||||
|
assert.Equal(t, "", message.ApplicationProperties["cloudEvents:tracestate"])
|
||||||
|
|
||||||
|
// Check deserialized message
|
||||||
|
var auditEvent internalAuditApi.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://stackit-platform/t/swz/audit-log/eu01/v1/resource-manager/system-changed"
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
// Instantiate audit api
|
||||||
|
auditApi, err := NewLegacyAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
StaticTopicNameConfig{TopicName: topicName},
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event := internalAuditApi.NewSystemAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||||
|
assert.NoError(t,
|
||||||
|
auditApi.Log(
|
||||||
|
ctx,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.RoutableSystemIdentifier,
|
||||||
|
))
|
||||||
|
|
||||||
|
// 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.Equal(t, "", message.ApplicationProperties["cloudEvents:traceparent"])
|
||||||
|
assert.Equal(t, "", message.ApplicationProperties["cloudEvents:tracestate"])
|
||||||
|
|
||||||
|
// Check deserialized message
|
||||||
|
var auditEvent internalAuditApi.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://stackit-platform/t/swz/audit-log/eu01/v1/resource-manager/organization-created"
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
// Instantiate audit api
|
||||||
|
auditApi, err := NewLegacyAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
StaticTopicNameConfig{TopicName: topicName},
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event, objectIdentifier := internalAuditApi.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.Log(
|
||||||
|
ctx,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier),
|
||||||
|
))
|
||||||
|
|
||||||
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
validateSentMessageWithDetails(t, topicName, message, event)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateSentMessage(
|
||||||
|
t *testing.T,
|
||||||
|
topicName string,
|
||||||
|
message *amqp.Message,
|
||||||
|
event *auditV1.AuditLogEntry,
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Check message properties
|
||||||
|
assert.Equal(t, topicName, *message.Properties.To)
|
||||||
|
assert.Equal(t, "", message.ApplicationProperties["cloudEvents:traceparent"])
|
||||||
|
assert.Equal(t, "", message.ApplicationProperties["cloudEvents:tracestate"])
|
||||||
|
|
||||||
|
// Check deserialized message
|
||||||
|
var auditEvent internalAuditApi.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,
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Check topic name
|
||||||
|
assert.Equal(t, topicName, *message.Properties.To)
|
||||||
|
assert.Equal(t, pkgAuditCommon.ContentTypeCloudEventsJson, message.ApplicationProperties["cloudEvents:datacontenttype"])
|
||||||
|
assert.Equal(t, DataTypeLegacyAuditEventV1, message.ApplicationProperties["cloudEvents:type"])
|
||||||
|
assert.Equal(t, "", message.ApplicationProperties["cloudEvents:traceparent"])
|
||||||
|
assert.Equal(t, "", message.ApplicationProperties["cloudEvents:tracestate"])
|
||||||
|
|
||||||
|
// Check deserialized message
|
||||||
|
var auditEvent internalAuditApi.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, StaticTopicNameConfig{}, 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 := pkgMessagingTest.NewSolaceContainer(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer solaceContainer.Stop()
|
||||||
|
|
||||||
|
// Instantiate the messaging api
|
||||||
|
amqpApi, err := pkgMessagingApi.NewAmqpApi(pkgMessagingCommon.AmqpConnectionPoolConfig{
|
||||||
|
Parameters: pkgMessagingCommon.AmqpConnectionConfig{BrokerUrl: solaceContainer.AmqpConnectionString},
|
||||||
|
PoolSize: 1,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Validator
|
||||||
|
validator, err := protovalidate.New()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
auditApi, err := NewLegacyAuditApi(amqpApi, StaticTopicNameConfig{
|
||||||
|
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 pkgAuditCommon.ProtobufValidator = validator
|
||||||
|
|
||||||
|
auditApi := LegacyAuditApi{
|
||||||
|
tracer: otel.Tracer("test"),
|
||||||
|
validator: protobufValidator,
|
||||||
|
}
|
||||||
|
|
||||||
|
event := internalAuditApi.NewSystemAuditEvent(nil)
|
||||||
|
_, err := auditApi.ValidateAndSerialize(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.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 pkgAuditCommon.ProtobufValidator = validator
|
||||||
|
|
||||||
|
auditApi := LegacyAuditApi{
|
||||||
|
tracer: otel.Tracer("test"),
|
||||||
|
validator: protobufValidator,
|
||||||
|
}
|
||||||
|
|
||||||
|
event := internalAuditApi.NewSystemAuditEvent(nil)
|
||||||
|
err := auditApi.Log(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.RoutableSystemIdentifier)
|
||||||
|
assert.ErrorIs(t, err, expectedError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLegacyAuditApi_Log_NilEvent(t *testing.T) {
|
||||||
|
auditApi := LegacyAuditApi{tracer: otel.Tracer("test")}
|
||||||
|
err := auditApi.Log(context.Background(), nil, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.RoutableSystemIdentifier)
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.ErrEventNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLegacyAuditApi_ConvertAndSerializeIntoLegacyFormatInvalidObjectIdentifierType(t *testing.T) {
|
||||||
|
customization := func(event *auditV1.AuditLogEntry,
|
||||||
|
objectIdentifier *auditV1.ObjectIdentifier) {
|
||||||
|
objectIdentifier.Type = "invalid"
|
||||||
|
}
|
||||||
|
event, objectIdentifier := internalAuditApi.NewProjectAuditEvent(&customization)
|
||||||
|
|
||||||
|
validator := &ProtobufValidatorMock{}
|
||||||
|
validator.On("Validate", mock.Anything).Return(nil)
|
||||||
|
var protobufValidator pkgAuditCommon.ProtobufValidator = validator
|
||||||
|
|
||||||
|
auditApi := LegacyAuditApi{
|
||||||
|
tracer: otel.Tracer("test"),
|
||||||
|
validator: protobufValidator,
|
||||||
|
}
|
||||||
|
_, err := auditApi.ValidateAndSerialize(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.NewRoutableIdentifier(objectIdentifier))
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.ErrUnsupportedRoutableType)
|
||||||
|
}
|
||||||
98
pkg/audit/api/api_mock.go
Normal file
98
pkg/audit/api/api_mock.go
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"buf.build/go/protovalidate"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
internalApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/internal/audit/api"
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockAuditApi is an implementation of AuditApi that does nothing and has no dependency to external systems.
|
||||||
|
type MockAuditApi struct {
|
||||||
|
tracer trace.Tracer
|
||||||
|
validator pkgAuditCommon.ProtobufValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMockAuditApi() (pkgAuditCommon.AuditApi, error) {
|
||||||
|
validator, err := protovalidate.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var protobufValidator pkgAuditCommon.ProtobufValidator = validator
|
||||||
|
var auditApi pkgAuditCommon.AuditApi = &MockAuditApi{
|
||||||
|
tracer: otel.Tracer("mock-audit-api"),
|
||||||
|
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 *pkgAuditCommon.RoutableIdentifier,
|
||||||
|
) error {
|
||||||
|
|
||||||
|
_, err := a.ValidateAndSerialize(ctx, event, visibility, routableIdentifier)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAndSerialize implements AuditApi.ValidateAndSerialize
|
||||||
|
func (a *MockAuditApi) ValidateAndSerialize(
|
||||||
|
ctx context.Context,
|
||||||
|
event *auditV1.AuditLogEntry,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
routableIdentifier *pkgAuditCommon.RoutableIdentifier,
|
||||||
|
) (*pkgAuditCommon.CloudEvent, error) {
|
||||||
|
|
||||||
|
ctx, span := a.tracer.Start(ctx, "validate-and-serialize")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
routableEvent, err := internalApi.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(pkgAuditCommon.EventTypeDataAccess)) {
|
||||||
|
return nil, pkgAuditCommon.ErrUnsupportedEventTypeDataAccess
|
||||||
|
}
|
||||||
|
|
||||||
|
routableEventBytes, err := proto.Marshal(routableEvent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
traceParent, traceState := internalApi.TraceParentAndStateFromContext(ctx)
|
||||||
|
|
||||||
|
message := pkgAuditCommon.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, *pkgAuditCommon.RoutableIdentifier, *pkgAuditCommon.CloudEvent) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
80
pkg/audit/api/api_mock_test.go
Normal file
80
pkg/audit/api/api_mock_test.go
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"buf.build/go/protovalidate"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
internalAuditApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/internal/audit/api"
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProtobufValidatorMock struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ProtobufValidatorMock) Validate(msg proto.Message, options ...protovalidate.ValidationOption) error {
|
||||||
|
var args mock.Arguments
|
||||||
|
if len(options) > 0 {
|
||||||
|
args = m.Called(msg, options)
|
||||||
|
} else {
|
||||||
|
args = m.Called(msg)
|
||||||
|
}
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMockAuditApi_Log(t *testing.T) {
|
||||||
|
|
||||||
|
auditApi, err := NewMockAuditApi()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate test data
|
||||||
|
event, objectIdentifier := internalAuditApi.NewOrganizationAuditEvent(nil)
|
||||||
|
routableObjectIdentifier := pkgAuditCommon.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) {
|
||||||
|
orgEvent, objIdentifier := internalAuditApi.NewOrganizationAuditEvent(nil)
|
||||||
|
orgEvent.LogName = strings.Replace(orgEvent.LogName, string(pkgAuditCommon.EventTypeAdminActivity), string(pkgAuditCommon.EventTypeDataAccess), 1)
|
||||||
|
rtIdentifier := pkgAuditCommon.NewRoutableIdentifier(objIdentifier)
|
||||||
|
|
||||||
|
assert.ErrorIs(t, auditApi.Log(
|
||||||
|
context.Background(), orgEvent, auditV1.Visibility_VISIBILITY_PUBLIC, rtIdentifier),
|
||||||
|
pkgAuditCommon.ErrUnsupportedEventTypeDataAccess)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ValidateAndSerialize", func(t *testing.T) {
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||||
|
cloudEvent, err := auditApi.ValidateAndSerialize(
|
||||||
|
context.Background(), event, visibility, routableObjectIdentifier)
|
||||||
|
|
||||||
|
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(context.Background(), nil, visibility, routableObjectIdentifier)
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.ErrEventNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Send", func(t *testing.T) {
|
||||||
|
var cloudEvent = pkgAuditCommon.CloudEvent{}
|
||||||
|
|
||||||
|
assert.Nil(t, auditApi.Send(context.Background(), routableObjectIdentifier, &cloudEvent))
|
||||||
|
})
|
||||||
|
}
|
||||||
191
pkg/audit/api/api_routable.go
Normal file
191
pkg/audit/api/api_routable.go
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
internalAuditApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/internal/audit/api"
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
pkgMessagingApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 *pkgAuditCommon.RoutableIdentifier) (string, error) {
|
||||||
|
|
||||||
|
if routableIdentifier == nil {
|
||||||
|
return "", pkgAuditCommon.ErrObjectIdentifierNil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch routableIdentifier.Type {
|
||||||
|
case pkgAuditCommon.ObjectTypeOrganization:
|
||||||
|
return fmt.Sprintf("topic://%s/%s", r.organizationTopicPrefix, routableIdentifier.Identifier), nil
|
||||||
|
case pkgAuditCommon.ObjectTypeProject:
|
||||||
|
return fmt.Sprintf("topic://%s/%s", r.projectTopicPrefix, routableIdentifier.Identifier), nil
|
||||||
|
case pkgAuditCommon.ObjectTypeFolder:
|
||||||
|
return fmt.Sprintf("topic://%s/%s", r.folderTopicPrefix, routableIdentifier.Identifier), nil
|
||||||
|
case pkgAuditCommon.ObjectTypeSystem:
|
||||||
|
return r.systemTopicName, nil
|
||||||
|
default:
|
||||||
|
return "", pkgAuditCommon.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 pkgMessagingApi.Api
|
||||||
|
topicNameResolver pkgAuditCommon.TopicNameResolver
|
||||||
|
tracer trace.Tracer
|
||||||
|
validator pkgAuditCommon.ProtobufValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRoutableAuditApi can be used to initialize the audit log api.
|
||||||
|
func newRoutableAuditApi(
|
||||||
|
messagingApi pkgMessagingApi.Api,
|
||||||
|
topicNameConfig topicNameConfig,
|
||||||
|
validator pkgAuditCommon.ProtobufValidator,
|
||||||
|
) (pkgAuditCommon.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 pkgAuditCommon.TopicNameResolver = &routableTopicNameResolver{
|
||||||
|
folderTopicPrefix: topicNameConfig.FolderTopicPrefix,
|
||||||
|
organizationTopicPrefix: topicNameConfig.OrganizationTopicPrefix,
|
||||||
|
projectTopicPrefix: topicNameConfig.ProjectTopicPrefix,
|
||||||
|
systemTopicName: topicNameConfig.SystemTopicName,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audit api
|
||||||
|
var auditApi pkgAuditCommon.AuditApi = &routableAuditApi{
|
||||||
|
messagingApi: messagingApi,
|
||||||
|
topicNameResolver: topicNameResolver,
|
||||||
|
tracer: otel.Tracer("routable-audit-api"),
|
||||||
|
validator: validator,
|
||||||
|
}
|
||||||
|
|
||||||
|
return auditApi, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log implements AuditApi.Log
|
||||||
|
func (a *routableAuditApi) Log(
|
||||||
|
ctx context.Context,
|
||||||
|
event *auditV1.AuditLogEntry,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
routableIdentifier *pkgAuditCommon.RoutableIdentifier,
|
||||||
|
) error {
|
||||||
|
|
||||||
|
cloudEvent, err := a.ValidateAndSerialize(ctx, event, visibility, routableIdentifier)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.Send(ctx, routableIdentifier, cloudEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAndSerialize implements AuditApi.ValidateAndSerialize
|
||||||
|
func (a *routableAuditApi) ValidateAndSerialize(
|
||||||
|
ctx context.Context,
|
||||||
|
event *auditV1.AuditLogEntry,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
routableIdentifier *pkgAuditCommon.RoutableIdentifier,
|
||||||
|
) (*pkgAuditCommon.CloudEvent, error) {
|
||||||
|
|
||||||
|
ctx, span := a.tracer.Start(ctx, "validate-and-serialize")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
routableEvent, err := internalAuditApi.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(pkgAuditCommon.EventTypeDataAccess)) {
|
||||||
|
return nil, pkgAuditCommon.ErrUnsupportedEventTypeDataAccess
|
||||||
|
}
|
||||||
|
|
||||||
|
routableEventBytes, err := proto.Marshal(routableEvent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
traceParent, traceState := internalAuditApi.TraceParentAndStateFromContext(ctx)
|
||||||
|
|
||||||
|
message := pkgAuditCommon.CloudEvent{
|
||||||
|
SpecVersion: "1.0",
|
||||||
|
Source: event.ProtoPayload.ServiceName,
|
||||||
|
Id: event.InsertId,
|
||||||
|
Time: event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(),
|
||||||
|
DataContentType: pkgAuditCommon.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 *pkgAuditCommon.RoutableIdentifier,
|
||||||
|
cloudEvent *pkgAuditCommon.CloudEvent,
|
||||||
|
) error {
|
||||||
|
|
||||||
|
if cloudEvent != nil && cloudEvent.TraceParent != nil && cloudEvent.TraceState != nil {
|
||||||
|
ctx = internalAuditApi.AddTraceParentAndStateToContext(ctx, *cloudEvent.TraceParent, *cloudEvent.TraceState)
|
||||||
|
}
|
||||||
|
ctx, span := a.tracer.Start(ctx, "send")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
return internalAuditApi.Send(a.topicNameResolver, a.messagingApi, ctx, routableIdentifier, cloudEvent)
|
||||||
|
}
|
||||||
540
pkg/audit/api/api_routable_test.go
Normal file
540
pkg/audit/api/api_routable_test.go
Normal file
|
|
@ -0,0 +1,540 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"buf.build/go/protovalidate"
|
||||||
|
"github.com/Azure/go-amqp"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
internalAuditApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/internal/audit/api"
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
pkgMessagingApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/api"
|
||||||
|
pgkMessagingCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/common"
|
||||||
|
pkgMessagingTest "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 := pkgMessagingTest.NewSolaceContainer(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer solaceContainer.Stop()
|
||||||
|
|
||||||
|
// Instantiate the messaging api
|
||||||
|
amqpApi, err := pkgMessagingApi.NewAmqpApi(pgkMessagingCommon.AmqpConnectionPoolConfig{
|
||||||
|
Parameters: pgkMessagingCommon.AmqpConnectionConfig{BrokerUrl: solaceContainer.AmqpConnectionString},
|
||||||
|
PoolSize: 1,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Validator
|
||||||
|
validator, err := protovalidate.New()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Instantiate the audit api
|
||||||
|
organizationTopicPrefix := "org"
|
||||||
|
projectTopicPrefix := "project"
|
||||||
|
folderTopicPrefix := "folder"
|
||||||
|
systemTopicName := "topic://system/admin-events"
|
||||||
|
|
||||||
|
auditApi, err := newRoutableAuditApi(
|
||||||
|
amqpApi,
|
||||||
|
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 := internalAuditApi.NewOrganizationAuditEvent(nil)
|
||||||
|
event.LogName = strings.Replace(event.LogName, string(pkgAuditCommon.EventTypeAdminActivity), string(pkgAuditCommon.EventTypeDataAccess), 1)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||||
|
assert.ErrorIs(t, auditApi.Log(
|
||||||
|
ctx,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)), pkgAuditCommon.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 := internalAuditApi.NewOrganizationAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||||
|
assert.NoError(t, auditApi.Log(
|
||||||
|
ctx,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)))
|
||||||
|
|
||||||
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
validateSentEvent(
|
||||||
|
t,
|
||||||
|
organizationTopicPrefix,
|
||||||
|
message,
|
||||||
|
objectIdentifier,
|
||||||
|
event,
|
||||||
|
"stackit.resourcemanager.v2.organization.created",
|
||||||
|
visibility)
|
||||||
|
})
|
||||||
|
|
||||||
|
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 := internalAuditApi.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.Log(
|
||||||
|
ctx,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)))
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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 := internalAuditApi.NewFolderAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||||
|
assert.NoError(t, auditApi.Log(
|
||||||
|
ctx,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)))
|
||||||
|
|
||||||
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
validateSentEvent(
|
||||||
|
t,
|
||||||
|
folderTopicPrefix,
|
||||||
|
message,
|
||||||
|
objectIdentifier,
|
||||||
|
event,
|
||||||
|
"stackit.resourcemanager.v2.folder.created",
|
||||||
|
visibility)
|
||||||
|
})
|
||||||
|
|
||||||
|
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 := internalAuditApi.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.Log(
|
||||||
|
ctx,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)))
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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 := internalAuditApi.NewProjectAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||||
|
assert.NoError(t,
|
||||||
|
auditApi.Log(
|
||||||
|
ctx,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)))
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
})
|
||||||
|
|
||||||
|
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 := internalAuditApi.NewProjectAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||||
|
assert.NoError(t,
|
||||||
|
auditApi.Log(
|
||||||
|
ctx,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)))
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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 := internalAuditApi.NewProjectSystemAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||||
|
assert.NoError(t,
|
||||||
|
auditApi.Log(
|
||||||
|
ctx,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.RoutableSystemIdentifier,
|
||||||
|
))
|
||||||
|
|
||||||
|
// 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.Equal(t, "", applicationProperties["cloudEvents:traceparent"])
|
||||||
|
assert.Equal(t, "", applicationProperties["cloudEvents:tracestate"])
|
||||||
|
|
||||||
|
// Check deserialized message
|
||||||
|
validateRoutableEventPayload(
|
||||||
|
t,
|
||||||
|
message.Data[0],
|
||||||
|
pkgAuditCommon.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 := internalAuditApi.NewSystemAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||||
|
assert.NoError(t,
|
||||||
|
auditApi.Log(
|
||||||
|
ctx,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.RoutableSystemIdentifier,
|
||||||
|
))
|
||||||
|
|
||||||
|
// 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.Equal(t, "", applicationProperties["cloudEvents:traceparent"])
|
||||||
|
assert.Equal(t, "", applicationProperties["cloudEvents:tracestate"])
|
||||||
|
|
||||||
|
// Check deserialized message
|
||||||
|
validateRoutableEventPayload(
|
||||||
|
t,
|
||||||
|
message.Data[0],
|
||||||
|
pkgAuditCommon.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 := internalAuditApi.NewOrganizationAuditEvent(nil)
|
||||||
|
|
||||||
|
// Log the event to solace
|
||||||
|
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||||
|
assert.NoError(t, auditApi.Log(
|
||||||
|
ctx,
|
||||||
|
event,
|
||||||
|
visibility,
|
||||||
|
pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)))
|
||||||
|
|
||||||
|
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
validateSentEvent(
|
||||||
|
t,
|
||||||
|
organizationTopicPrefix,
|
||||||
|
message,
|
||||||
|
objectIdentifier,
|
||||||
|
event,
|
||||||
|
"stackit.resourcemanager.v2.organization.created",
|
||||||
|
visibility)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateSentEvent(
|
||||||
|
t *testing.T,
|
||||||
|
topicPrefix string,
|
||||||
|
message *amqp.Message,
|
||||||
|
objectIdentifier *auditV1.ObjectIdentifier,
|
||||||
|
event *auditV1.AuditLogEntry,
|
||||||
|
operationName string,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
) {
|
||||||
|
|
||||||
|
// 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, pkgAuditCommon.ContentTypeCloudEventsProtobuf, applicationProperties["cloudEvents:datacontenttype"])
|
||||||
|
assert.Equal(t, "audit.v1.RoutableAuditEvent", applicationProperties["cloudEvents:type"])
|
||||||
|
assert.Equal(t, "", applicationProperties["cloudEvents:traceparent"])
|
||||||
|
assert.Equal(t, "", 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(pkgAuditCommon.NewRoutableIdentifier(&auditV1.ObjectIdentifier{Type: "unsupported"}))
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.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 pkgAuditCommon.ProtobufValidator = validator
|
||||||
|
|
||||||
|
auditApi := routableAuditApi{
|
||||||
|
tracer: otel.Tracer("test"),
|
||||||
|
validator: protobufValidator,
|
||||||
|
}
|
||||||
|
|
||||||
|
event := internalAuditApi.NewSystemAuditEvent(nil)
|
||||||
|
_, err := auditApi.ValidateAndSerialize(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.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 pkgAuditCommon.ProtobufValidator = validator
|
||||||
|
|
||||||
|
auditApi := routableAuditApi{
|
||||||
|
tracer: otel.Tracer("test"),
|
||||||
|
validator: protobufValidator,
|
||||||
|
}
|
||||||
|
|
||||||
|
event := internalAuditApi.NewSystemAuditEvent(nil)
|
||||||
|
err := auditApi.Log(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.RoutableSystemIdentifier)
|
||||||
|
assert.ErrorIs(t, err, expectedError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoutableAuditApi_Log_NilEvent(t *testing.T) {
|
||||||
|
auditApi := routableAuditApi{tracer: otel.Tracer("test")}
|
||||||
|
err := auditApi.Log(context.Background(), nil, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.RoutableSystemIdentifier)
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.ErrEventNil)
|
||||||
|
}
|
||||||
72
pkg/audit/api/base64.go
Normal file
72
pkg/audit/api/base64.go
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 pkgAuditCommon.CloudEvent `json:"cloudEvent"`
|
||||||
|
RoutableIdentifier pkgAuditCommon.RoutableIdentifier `json:"routableIdentifier"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToBase64(
|
||||||
|
cloudEvent *pkgAuditCommon.CloudEvent,
|
||||||
|
routableIdentifier *pkgAuditCommon.RoutableIdentifier) (*string, error) {
|
||||||
|
|
||||||
|
if cloudEvent == nil {
|
||||||
|
return nil, pkgAuditCommon.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 += base64AuditEventV1
|
||||||
|
return &base64Str, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromBase64(base64Str string) (*pkgAuditCommon.CloudEvent, *pkgAuditCommon.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
|
||||||
|
}
|
||||||
88
pkg/audit/api/base64_test.go
Normal file
88
pkg/audit/api/base64_test.go
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_ToBase64(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("cloud event nil", func(t *testing.T) {
|
||||||
|
var cloudEvent *pkgAuditCommon.CloudEvent = nil
|
||||||
|
routableIdentifier := pkgAuditCommon.RoutableSystemIdentifier
|
||||||
|
|
||||||
|
base64str, err := ToBase64(cloudEvent, routableIdentifier)
|
||||||
|
assert.ErrorIs(t, err, pkgAuditCommon.ErrCloudEventNil)
|
||||||
|
assert.Nil(t, base64str)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("routable identifier nil", func(t *testing.T) {
|
||||||
|
cloudEvent := &pkgAuditCommon.CloudEvent{}
|
||||||
|
var routableIdentifier *pkgAuditCommon.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 := &pkgAuditCommon.CloudEvent{}
|
||||||
|
r := pkgAuditCommon.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 := &pkgAuditCommon.CloudEvent{}
|
||||||
|
r := pkgAuditCommon.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)
|
||||||
|
})
|
||||||
|
}
|
||||||
683
pkg/audit/api/builder.go
Normal file
683
pkg/audit/api/builder.go
Normal file
|
|
@ -0,0 +1,683 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
internalAuditApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/internal/audit/api"
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
pkgAuditUtils "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 pkgAuditCommon.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 pkgAuditCommon.ObjectType
|
||||||
|
|
||||||
|
ResponseBody any
|
||||||
|
|
||||||
|
// Log severity
|
||||||
|
Severity auditV1.LogSeverity
|
||||||
|
}
|
||||||
|
|
||||||
|
func getObjectIdAndTypeFromAuditParams(
|
||||||
|
auditParams *AuditParameters,
|
||||||
|
) (string, *pkgAuditCommon.ObjectType, error) {
|
||||||
|
|
||||||
|
objectId := auditParams.ObjectId
|
||||||
|
if objectId == "" {
|
||||||
|
return "", nil, errors.New("object id missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
var objectType *pkgAuditCommon.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 internalAuditApi.AuditRequest
|
||||||
|
auditResponse internalAuditApi.AuditResponse
|
||||||
|
auditMetadata internalAuditApi.AuditMetadata
|
||||||
|
|
||||||
|
// Region and optional zone id. If both, separated with a - (dash).
|
||||||
|
// Example: eu01
|
||||||
|
location string
|
||||||
|
|
||||||
|
// Opentelemetry tracer
|
||||||
|
tracer trace.Tracer
|
||||||
|
|
||||||
|
// 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: pkgAuditCommon.EventTypeAdminActivity,
|
||||||
|
},
|
||||||
|
auditRequest: internalAuditApi.AuditRequest{
|
||||||
|
Request: &pkgAuditCommon.ApiRequest{},
|
||||||
|
RequestClientIP: quadZero,
|
||||||
|
RequestCorrelationId: nil,
|
||||||
|
RequestId: nil,
|
||||||
|
RequestTime: &requestTime,
|
||||||
|
},
|
||||||
|
auditResponse: internalAuditApi.AuditResponse{
|
||||||
|
ResponseBodyBytes: nil,
|
||||||
|
ResponseStatusCode: 200,
|
||||||
|
ResponseHeaders: make(map[string][]string),
|
||||||
|
ResponseNumItems: nil,
|
||||||
|
ResponseTime: nil,
|
||||||
|
},
|
||||||
|
auditMetadata: internalAuditApi.AuditMetadata{
|
||||||
|
AuditInsertId: "",
|
||||||
|
AuditLabels: nil,
|
||||||
|
AuditLogName: "",
|
||||||
|
AuditLogSeverity: auditV1.LogSeverity_LOG_SEVERITY_DEFAULT,
|
||||||
|
AuditOperationName: "",
|
||||||
|
AuditPermission: nil,
|
||||||
|
AuditPermissionGranted: nil,
|
||||||
|
AuditResourceName: "",
|
||||||
|
AuditServiceName: "",
|
||||||
|
AuditTime: nil,
|
||||||
|
},
|
||||||
|
location: "",
|
||||||
|
tracer: otel.Tracer("audit-log-entry-builder"),
|
||||||
|
workerId: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (builder *AuditLogEntryBuilder) AsSystemEvent() *AuditLogEntryBuilder {
|
||||||
|
if builder.auditRequest.Request == nil {
|
||||||
|
builder.auditRequest.Request = &pkgAuditCommon.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(pkgAuditCommon.EventTypeSystemEvent)
|
||||||
|
return builder
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRequiredApiRequest adds api request details
|
||||||
|
func (builder *AuditLogEntryBuilder) WithRequiredApiRequest(request pkgAuditCommon.ApiRequest) *AuditLogEntryBuilder {
|
||||||
|
builder.auditRequest.Request = &request
|
||||||
|
return builder
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetApiRequest returns the api request details
|
||||||
|
func (builder *AuditLogEntryBuilder) GetApiRequest() *pkgAuditCommon.ApiRequest {
|
||||||
|
return builder.auditRequest.Request
|
||||||
|
}
|
||||||
|
|
||||||
|
func (builder *AuditLogEntryBuilder) GetApiRequestBody() *pkgAuditCommon.ApiRequest {
|
||||||
|
return builder.auditRequest.Request
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 pkgAuditCommon.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 pkgAuditCommon.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(ctx context.Context, sequenceNumber SequenceNumber) (*auditV1.AuditLogEntry, error) {
|
||||||
|
_, span := builder.tracer.Start(ctx, "build-audit-log-entry")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
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 pkgAuditCommon.ObjectType
|
||||||
|
if builder.auditParams.EventType == pkgAuditCommon.EventTypeSystemEvent {
|
||||||
|
logIdentifier = pkgAuditCommon.SystemIdentifier.Identifier
|
||||||
|
logType = pkgAuditCommon.ObjectTypeSystem
|
||||||
|
} else {
|
||||||
|
logIdentifier = objectId
|
||||||
|
logType = *objectType
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.auditMetadata.AuditInsertId = internalAuditApi.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{}
|
||||||
|
if len(builder.auditParams.Details) > 0 {
|
||||||
|
details = builder.auditParams.Details
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instantiate the audit event
|
||||||
|
return internalAuditApi.NewAuditLogEntry(
|
||||||
|
builder.auditRequest,
|
||||||
|
builder.auditResponse,
|
||||||
|
details,
|
||||||
|
builder.auditMetadata,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 pkgAuditCommon.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 pkgAuditUtils.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 pkgAuditCommon.AuditApi,
|
||||||
|
|
||||||
|
// The sequence number generator can be used to get and revert sequence numbers to build audit log events
|
||||||
|
sequenceNumberGenerator pkgAuditUtils.SequenceNumberGenerator,
|
||||||
|
|
||||||
|
// 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: otel.Tracer("audit-event-builder"),
|
||||||
|
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(number SequenceNumber) {
|
||||||
|
builder.sequenceNumberGenerator.Revert(uint64(number))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 pkgAuditCommon.ApiRequest) *AuditEventBuilder {
|
||||||
|
builder.auditLogEntryBuilder.WithRequiredApiRequest(request)
|
||||||
|
return builder
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetApiRequest returns the api request details
|
||||||
|
func (builder *AuditEventBuilder) GetApiRequest() *pkgAuditCommon.ApiRequest {
|
||||||
|
return builder.auditLogEntryBuilder.GetApiRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 pkgAuditCommon.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 pkgAuditCommon.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
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkAsBuilt sets the internal built flag to true.
|
||||||
|
// This is useful in middlewares where the IsBuilt method is used
|
||||||
|
// to check if an event has been constructed.
|
||||||
|
func (builder *AuditEventBuilder) MarkAsBuilt() {
|
||||||
|
builder.built = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) (*pkgAuditCommon.CloudEvent, *pkgAuditCommon.RoutableIdentifier, error) {
|
||||||
|
if builder.auditLogEntryBuilder == nil {
|
||||||
|
return nil, nil, fmt.Errorf("audit log entry builder not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, span := builder.tracer.Start(ctx, "build-audit-event")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
visibility := builder.visibility
|
||||||
|
objectId := builder.auditLogEntryBuilder.auditParams.ObjectId
|
||||||
|
objectType := builder.auditLogEntryBuilder.auditParams.ObjectType
|
||||||
|
var routingIdentifier *pkgAuditCommon.RoutableIdentifier
|
||||||
|
if builder.auditLogEntryBuilder.auditParams.EventType == pkgAuditCommon.EventTypeSystemEvent {
|
||||||
|
routingIdentifier = internalAuditApi.NewAuditRoutingIdentifier(uuid.Nil.String(), pkgAuditCommon.ObjectTypeSystem)
|
||||||
|
if objectId == "" {
|
||||||
|
objectId = uuid.Nil.String()
|
||||||
|
builder.WithRequiredObjectId(objectId)
|
||||||
|
}
|
||||||
|
if objectType == "" {
|
||||||
|
objectType = pkgAuditCommon.ObjectTypeSystem
|
||||||
|
builder.WithRequiredObjectType(objectType)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
routingIdentifier = internalAuditApi.NewAuditRoutingIdentifier(objectId, objectType)
|
||||||
|
}
|
||||||
|
|
||||||
|
auditLogEntry, err := builder.auditLogEntryBuilder.Build(ctx, sequenceNumber)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and serialize the protobuf event into a cloud event
|
||||||
|
cloudEvent, err := builder.api.ValidateAndSerialize(ctx, auditLogEntry, visibility, routingIdentifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.built = true
|
||||||
|
return cloudEvent,
|
||||||
|
routingIdentifier,
|
||||||
|
nil
|
||||||
|
}
|
||||||
1381
pkg/audit/api/builder_test.go
Normal file
1381
pkg/audit/api/builder_test.go
Normal file
File diff suppressed because it is too large
Load diff
103
pkg/audit/api/log.go
Normal file
103
pkg/audit/api/log.go
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
pkgLog "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogEvent logs an event to the terminal
|
||||||
|
func LogEvent(event *pkgAuditCommon.CloudEvent) error {
|
||||||
|
|
||||||
|
if event.DataType == DataTypeLegacyAuditEventV1 {
|
||||||
|
pkgLog.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
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgLog.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{}
|
||||||
|
}
|
||||||
103
pkg/audit/api/log_test.go
Normal file
103
pkg/audit/api/log_test.go
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"buf.build/go/protovalidate"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
internalAuditApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/internal/audit/api"
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
pkgAuditUtils "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_LogEvent(t *testing.T) {
|
||||||
|
|
||||||
|
api, _ := NewMockAuditApi()
|
||||||
|
sequenceNumberGenerator := pkgAuditUtils.NewDefaultSequenceNumberGenerator()
|
||||||
|
|
||||||
|
t.Run("new format", func(t *testing.T) {
|
||||||
|
eventBuilder := NewAuditEventBuilder(api, sequenceNumberGenerator, "demo-service", uuid.NewString(), "eu01")
|
||||||
|
|
||||||
|
cloudEvent, _, err := eventBuilder.
|
||||||
|
WithRequiredApiRequest(pkgAuditCommon.ApiRequest{
|
||||||
|
Body: nil,
|
||||||
|
Header: internalAuditApi.TestHeaders,
|
||||||
|
Host: "localhost",
|
||||||
|
Method: "GET",
|
||||||
|
Scheme: "https",
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
URL: pkgAuditCommon.RequestUrl{
|
||||||
|
Path: "/",
|
||||||
|
RawQuery: nil,
|
||||||
|
},
|
||||||
|
}).
|
||||||
|
WithRequiredObjectId(uuid.NewString()).
|
||||||
|
WithRequiredObjectType(pkgAuditCommon.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(pkgAuditCommon.ApiRequest{
|
||||||
|
Body: nil,
|
||||||
|
Header: internalAuditApi.TestHeaders,
|
||||||
|
Host: "localhost",
|
||||||
|
Method: "GET",
|
||||||
|
Scheme: "https",
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
URL: pkgAuditCommon.RequestUrl{
|
||||||
|
Path: "/",
|
||||||
|
RawQuery: nil,
|
||||||
|
},
|
||||||
|
}).
|
||||||
|
WithRequiredLocation("eu01").
|
||||||
|
WithRequiredObjectId(objectId).
|
||||||
|
WithRequiredObjectType(pkgAuditCommon.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 pkgAuditCommon.ProtobufValidator = validator
|
||||||
|
|
||||||
|
routableIdentifier := pkgAuditCommon.RoutableIdentifier{
|
||||||
|
Identifier: objectId,
|
||||||
|
Type: pkgAuditCommon.ObjectTypeProject,
|
||||||
|
}
|
||||||
|
|
||||||
|
routableEvent, err := internalAuditApi.ValidateAndSerializePartially(protoValidator, entry, auditV1.Visibility_VISIBILITY_PUBLIC, &routableIdentifier)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
legacyBytes, err := internalAuditApi.ConvertAndSerializeIntoLegacyFormat(entry, routableEvent)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
cloudEvent := pkgAuditCommon.CloudEvent{
|
||||||
|
SpecVersion: "1.0",
|
||||||
|
Source: entry.ProtoPayload.ServiceName,
|
||||||
|
Id: entry.InsertId,
|
||||||
|
Time: entry.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(),
|
||||||
|
DataContentType: pkgAuditCommon.ContentTypeCloudEventsJson,
|
||||||
|
DataType: DataTypeLegacyAuditEventV1,
|
||||||
|
Subject: entry.ProtoPayload.ResourceName,
|
||||||
|
Data: legacyBytes,
|
||||||
|
TraceParent: nil,
|
||||||
|
TraceState: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, LogEvent(&cloudEvent))
|
||||||
|
})
|
||||||
|
}
|
||||||
91
pkg/audit/api/utils.go
Normal file
91
pkg/audit/api/utils.go
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
var objectTypeIdPattern = regexp.MustCompile(".*/(projects|folders|organizations)/([0-9a-fA-F-]{36})(?:/.*)?")
|
||||||
|
|
||||||
|
// GetCalledServiceNameFromRequest extracts the called service name from subdomain name
|
||||||
|
func GetCalledServiceNameFromRequest(request *pkgAuditCommon.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
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetObjectIdAndTypeFromUrlPath(path string) (
|
||||||
|
string,
|
||||||
|
*pkgAuditCommon.ObjectType,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Extract object id and type from request url
|
||||||
|
objectTypeIdMatches := objectTypeIdPattern.FindStringSubmatch(path)
|
||||||
|
if len(objectTypeIdMatches) > 0 {
|
||||||
|
objectType := pkgAuditCommon.ObjectTypeFromPluralString(objectTypeIdMatches[1])
|
||||||
|
err := objectType.IsSupportedType()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
objectId := objectTypeIdMatches[2]
|
||||||
|
|
||||||
|
return objectId, &objectType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
responseJson, err := json.Marshal(response)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return responseJson, 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
|
||||||
|
}
|
||||||
154
pkg/audit/api/utils_test.go
Normal file
154
pkg/audit/api/utils_test.go
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_GetCalledServiceNameFromRequest(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("request is nil", func(t *testing.T) {
|
||||||
|
serviceName := GetCalledServiceNameFromRequest(nil, "resource-manager")
|
||||||
|
assert.Equal(t, "resource-manager", serviceName)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("localhost", func(t *testing.T) {
|
||||||
|
request := pkgAuditCommon.ApiRequest{Host: "localhost:8080"}
|
||||||
|
serviceName := GetCalledServiceNameFromRequest(&request, "resource-manager")
|
||||||
|
assert.Equal(t, "resource-manager", serviceName)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("cf", func(t *testing.T) {
|
||||||
|
request := pkgAuditCommon.ApiRequest{Host: "stackit-resource-manager-go-dev.apps.01.cf.eu01.stackit.cloud"}
|
||||||
|
serviceName := GetCalledServiceNameFromRequest(&request, "resource-manager")
|
||||||
|
assert.Equal(t, "stackit-resource-manager-go-dev", serviceName)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("cf invalid host", func(t *testing.T) {
|
||||||
|
request := pkgAuditCommon.ApiRequest{Host: ""}
|
||||||
|
serviceName := GetCalledServiceNameFromRequest(&request, "resource-manager")
|
||||||
|
assert.Equal(t, "resource-manager", serviceName)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ip", func(t *testing.T) {
|
||||||
|
request := pkgAuditCommon.ApiRequest{Host: "127.0.0.1"}
|
||||||
|
serviceName := GetCalledServiceNameFromRequest(&request, "resource-manager")
|
||||||
|
assert.Equal(t, "resource-manager", serviceName)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("ip short", func(t *testing.T) {
|
||||||
|
request := pkgAuditCommon.ApiRequest{Host: "::1"}
|
||||||
|
serviceName := GetCalledServiceNameFromRequest(&request, "resource-manager")
|
||||||
|
assert.Equal(t, "resource-manager", serviceName)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_GetObjectIdAndTypeFromUrlPath(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("object id and type not in url", func(t *testing.T) {
|
||||||
|
objectId, objectType, err := GetObjectIdAndTypeFromUrlPath("/v2/projects/audit")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "", objectId)
|
||||||
|
assert.Nil(t, objectType)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("object id and type in url", func(t *testing.T) {
|
||||||
|
objectId, objectType, err := GetObjectIdAndTypeFromUrlPath("/v2/projects/f17d4064-9b65-4334-b6a7-8fed96340124")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "f17d4064-9b65-4334-b6a7-8fed96340124", objectId)
|
||||||
|
assert.Equal(t, pkgAuditCommon.ObjectTypeProject, *objectType)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("multiple object ids and types in url", func(t *testing.T) {
|
||||||
|
objectId, objectType, err := GetObjectIdAndTypeFromUrlPath("/v2/organization/8ee58bec-d496-4bb9-af8d-72fda4d78b6b/projects/f17d4064-9b65-4334-b6a7-8fed96340124")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "f17d4064-9b65-4334-b6a7-8fed96340124", objectId)
|
||||||
|
assert.Equal(t, pkgAuditCommon.ObjectTypeProject, *objectType)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ResponseBodyToBytes(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"nil response body", func(t *testing.T) {
|
||||||
|
bytes, err := ResponseBodyToBytes(nil)
|
||||||
|
assert.Nil(t, bytes)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"bytes", func(t *testing.T) {
|
||||||
|
responseBody := []byte("data")
|
||||||
|
bytes, err := ResponseBodyToBytes(responseBody)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, responseBody, bytes)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"Protobuf message", func(t *testing.T) {
|
||||||
|
protobufMessage := auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: string(pkgAuditCommon.ObjectTypeProject)}
|
||||||
|
bytes, err := ResponseBodyToBytes(&protobufMessage)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
expected, err := protojson.Marshal(&protobufMessage)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expected, bytes)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"struct", func(t *testing.T) {
|
||||||
|
type CustomObject struct {
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBody := CustomObject{Value: "data"}
|
||||||
|
bytes, err := ResponseBodyToBytes(responseBody)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
expected, err := json.Marshal(responseBody)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expected, bytes)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"map", func(t *testing.T) {
|
||||||
|
|
||||||
|
responseBody := map[string]interface{}{"value": "data"}
|
||||||
|
bytes, err := ResponseBodyToBytes(responseBody)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
expected, err := json.Marshal(responseBody)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expected, bytes)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ToArrayMap(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("empty map", func(t *testing.T) {
|
||||||
|
result := ToArrayMap(map[string]string{})
|
||||||
|
assert.Equal(t, map[string][]string{}, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty map", func(t *testing.T) {
|
||||||
|
result := ToArrayMap(map[string]string{"key1": "value1", "key2": "value2"})
|
||||||
|
assert.Equal(t, map[string][]string{
|
||||||
|
"key1": {"value1"},
|
||||||
|
"key2": {"value2"},
|
||||||
|
}, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
259
pkg/audit/common/api.go
Normal file
259
pkg/audit/common/api.go
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"buf.build/go/protovalidate"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContentTypeCloudEventsProtobuf the cloudevents protobuf content-type sent in metadata of messages
|
||||||
|
const ContentTypeCloudEventsProtobuf = "application/cloudevents+protobuf"
|
||||||
|
const ContentTypeCloudEventsJson = "application/cloudevents+json; charset=UTF-8"
|
||||||
|
|
||||||
|
var TopicNamePattern = regexp.MustCompile(`^topic://stackit-platform/t/swz/audit-log/(?:conway|eu01|eu02|sx-stoi01)/[Vv][1-9](?:\.\d)?/[A-Za-z0-9-]+/[A-Za-z0-9-/]+`)
|
||||||
|
|
||||||
|
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, ObjectTypeFolder, ObjectTypeProject, 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
|
||||||
|
|
||||||
|
// ValidateAndSerialize validates and serializes the event into a byte representation.
|
||||||
|
// The result has to be sent explicitly by calling the Send method.
|
||||||
|
//
|
||||||
|
// 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:
|
||||||
|
// * the CloudEvent (i.e. the serialized AuditLogEntry with metadata)
|
||||||
|
// * an error if validation or serialization failed
|
||||||
|
ValidateAndSerialize(
|
||||||
|
ctx context.Context,
|
||||||
|
event *auditV1.AuditLogEntry,
|
||||||
|
visibility auditV1.Visibility,
|
||||||
|
routableIdentifier *RoutableIdentifier,
|
||||||
|
) (*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, options ...protovalidate.ValidationOption) 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StaticTopicNameTestResolver implements TopicNameResolver.
|
||||||
|
// A hard-coded topic name is used, routable identifiers are ignored.
|
||||||
|
type StaticTopicNameTestResolver struct {
|
||||||
|
TopicName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve implements TopicNameResolver.Resolve
|
||||||
|
func (r *StaticTopicNameTestResolver) Resolve(*RoutableIdentifier) (string, error) {
|
||||||
|
return r.TopicName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
32
pkg/audit/common/converter.go
Normal file
32
pkg/audit/common/converter.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
49
pkg/audit/common/errors.go
Normal file
49
pkg/audit/common/errors.go
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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")
|
||||||
59
pkg/audit/common/model.go
Normal file
59
pkg/audit/common/model.go
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
60
pkg/audit/utils/sequence_generator.go
Normal file
60
pkg/audit/utils/sequence_generator.go
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SequenceNumberGenerator can be used to generate increasing numbers.
|
||||||
|
type SequenceNumberGenerator interface {
|
||||||
|
|
||||||
|
// Next returns the next number
|
||||||
|
Next() uint64
|
||||||
|
|
||||||
|
// Revert can be used to revert a specific number (e.g. in case of an error)
|
||||||
|
Revert(uint64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultSequenceNumberGenerator is a mutex protected implementation of SequenceNumberGenerator
|
||||||
|
type DefaultSequenceNumberGenerator struct {
|
||||||
|
backlog []uint64
|
||||||
|
sequenceNumber uint64
|
||||||
|
sequenceNumberLock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultSequenceNumberGenerator returns an instance of DefaultSequenceNumberGenerator as pointer
|
||||||
|
// of SequenceNumberGenerator.
|
||||||
|
func NewDefaultSequenceNumberGenerator() SequenceNumberGenerator {
|
||||||
|
var generator SequenceNumberGenerator = &DefaultSequenceNumberGenerator{
|
||||||
|
backlog: make([]uint64, 0),
|
||||||
|
sequenceNumber: 0,
|
||||||
|
sequenceNumberLock: sync.Mutex{},
|
||||||
|
}
|
||||||
|
return generator
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next implements SequenceNumberGenerator.Next
|
||||||
|
func (g *DefaultSequenceNumberGenerator) Next() uint64 {
|
||||||
|
g.sequenceNumberLock.Lock()
|
||||||
|
defer g.sequenceNumberLock.Unlock()
|
||||||
|
var next uint64
|
||||||
|
if len(g.backlog) == 0 {
|
||||||
|
next = g.sequenceNumber
|
||||||
|
g.sequenceNumber++
|
||||||
|
} else {
|
||||||
|
next = g.backlog[0]
|
||||||
|
g.backlog = g.backlog[1:]
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revert implements SequenceNumberGenerator.Revert
|
||||||
|
func (g *DefaultSequenceNumberGenerator) Revert(value uint64) {
|
||||||
|
g.sequenceNumberLock.Lock()
|
||||||
|
defer g.sequenceNumberLock.Unlock()
|
||||||
|
if value == g.sequenceNumber-1 {
|
||||||
|
g.sequenceNumber--
|
||||||
|
} else if !slices.Contains(g.backlog, value) {
|
||||||
|
g.backlog = append(g.backlog, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
59
pkg/audit/utils/sequence_generator_test.go
Normal file
59
pkg/audit/utils/sequence_generator_test.go
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
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(uint64(1))
|
||||||
|
assert.Equal(t, uint64(1), sequenceGenerator.Next())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("revert first", func(t *testing.T) {
|
||||||
|
var sequenceGenerator = NewDefaultSequenceNumberGenerator()
|
||||||
|
assert.Equal(t, uint64(0), sequenceGenerator.Next())
|
||||||
|
assert.Equal(t, uint64(1), sequenceGenerator.Next())
|
||||||
|
sequenceGenerator.Revert(uint64(0))
|
||||||
|
assert.Equal(t, uint64(0), sequenceGenerator.Next())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("revert same value multiple times", func(t *testing.T) {
|
||||||
|
var sequenceGenerator = NewDefaultSequenceNumberGenerator()
|
||||||
|
assert.Equal(t, uint64(0), sequenceGenerator.Next())
|
||||||
|
assert.Equal(t, uint64(1), sequenceGenerator.Next())
|
||||||
|
assert.Equal(t, uint64(2), sequenceGenerator.Next())
|
||||||
|
sequenceGenerator.Revert(uint64(1))
|
||||||
|
sequenceGenerator.Revert(uint64(1))
|
||||||
|
assert.Equal(t, uint64(1), sequenceGenerator.Next())
|
||||||
|
assert.Equal(t, uint64(3), sequenceGenerator.Next())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("get and revert multiple", func(t *testing.T) {
|
||||||
|
var sequenceGenerator = NewDefaultSequenceNumberGenerator()
|
||||||
|
assert.Equal(t, uint64(0), sequenceGenerator.Next())
|
||||||
|
assert.Equal(t, uint64(1), sequenceGenerator.Next())
|
||||||
|
sequenceGenerator.Revert(uint64(1))
|
||||||
|
assert.Equal(t, uint64(1), sequenceGenerator.Next())
|
||||||
|
assert.Equal(t, uint64(2), sequenceGenerator.Next())
|
||||||
|
assert.Equal(t, uint64(3), sequenceGenerator.Next())
|
||||||
|
assert.Equal(t, uint64(4), sequenceGenerator.Next())
|
||||||
|
sequenceGenerator.Revert(uint64(2))
|
||||||
|
sequenceGenerator.Revert(uint64(3))
|
||||||
|
assert.Equal(t, uint64(2), sequenceGenerator.Next())
|
||||||
|
assert.Equal(t, uint64(3), sequenceGenerator.Next())
|
||||||
|
assert.Equal(t, uint64(5), sequenceGenerator.Next())
|
||||||
|
assert.Equal(t, uint64(6), sequenceGenerator.Next())
|
||||||
|
})
|
||||||
|
}
|
||||||
28
pkg/log/log.go
Normal file
28
pkg/log/log.go
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
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
|
||||||
|
switch {
|
||||||
|
case len(err) == 0:
|
||||||
|
e = nil
|
||||||
|
case len(err) == 1:
|
||||||
|
e = err[0]
|
||||||
|
default:
|
||||||
|
e = errors.Join(err...)
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
40
pkg/log/log_test.go
Normal file
40
pkg/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
pkg/log/slog.go
Normal file
35
pkg/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
pkg/log/slog_test.go
Normal file
43
pkg/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
pkg/log/zerolog.go
Normal file
25
pkg/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
pkg/log/zerolog_test.go
Normal file
42
pkg/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"))
|
||||||
|
})
|
||||||
|
}
|
||||||
148
pkg/messaging/api/amqp.go
Normal file
148
pkg/messaging/api/amqp.go
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
internalMessaging "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/internal/messaging"
|
||||||
|
pkgLog "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/log"
|
||||||
|
pkgMessagingCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AmqpApi implements Api.
|
||||||
|
type AmqpApi struct {
|
||||||
|
connection *internalMessaging.AmqpConnection
|
||||||
|
connectionPool internalMessaging.ConnectionPool
|
||||||
|
connectionPoolHandle *internalMessaging.ConnectionPoolHandle
|
||||||
|
senderCache map[string]*internalMessaging.AmqpSenderSession
|
||||||
|
lock sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Api = &AmqpApi{}
|
||||||
|
|
||||||
|
func NewDefaultAmqpApi(amqpConfig pkgMessagingCommon.AmqpConnectionConfig) (Api, error) {
|
||||||
|
connectionPool, err := internalMessaging.NewDefaultAmqpConnectionPool(amqpConfig, "sdk")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("new amqp connection pool: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
amqpApi := &AmqpApi{
|
||||||
|
connectionPool: connectionPool,
|
||||||
|
connectionPoolHandle: connectionPool.NewHandle(),
|
||||||
|
senderCache: make(map[string]*internalMessaging.AmqpSenderSession),
|
||||||
|
}
|
||||||
|
|
||||||
|
var messagingApi Api = amqpApi
|
||||||
|
return messagingApi, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAmqpApi(amqpConfig pkgMessagingCommon.AmqpConnectionPoolConfig) (Api, error) {
|
||||||
|
connectionPool, err := internalMessaging.NewAmqpConnectionPool(amqpConfig, "sdk")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("new amqp connection pool: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
amqpApi := &AmqpApi{
|
||||||
|
connectionPool: connectionPool,
|
||||||
|
connectionPoolHandle: connectionPool.NewHandle(),
|
||||||
|
senderCache: make(map[string]*internalMessaging.AmqpSenderSession),
|
||||||
|
}
|
||||||
|
|
||||||
|
var messagingApi Api = amqpApi
|
||||||
|
return messagingApi, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send implements Api.Send.
|
||||||
|
// If errors occur the connection to the messaging system will be closed and re-established.
|
||||||
|
func (a *AmqpApi) Send(_ context.Context, topic string, data []byte, contentType string, applicationProperties map[string]any) error {
|
||||||
|
|
||||||
|
// create or get sender from cache
|
||||||
|
var sender = a.senderFromCache(topic)
|
||||||
|
if sender == nil {
|
||||||
|
if err := a.newSender(topic); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sender = a.senderFromCache(topic)
|
||||||
|
}
|
||||||
|
|
||||||
|
// first attempt to send
|
||||||
|
var sendErr error
|
||||||
|
wrappedData := [][]byte{data}
|
||||||
|
if err := sender.Send(topic, wrappedData, contentType, applicationProperties); err != nil {
|
||||||
|
sendErr = fmt.Errorf("send: %w", err)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// renew sender
|
||||||
|
if err := a.newSender(topic); err != nil {
|
||||||
|
return errors.Join(sendErr, err)
|
||||||
|
}
|
||||||
|
sender = a.senderFromCache(topic)
|
||||||
|
|
||||||
|
// retry send
|
||||||
|
if err := sender.Send(topic, wrappedData, contentType, applicationProperties); err != nil {
|
||||||
|
return errors.Join(sendErr, fmt.Errorf("retry send: %w", err))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AmqpApi) senderFromCache(topic string) *internalMessaging.AmqpSenderSession {
|
||||||
|
a.lock.RLock()
|
||||||
|
defer a.lock.RUnlock()
|
||||||
|
return a.senderCache[topic]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AmqpApi) newSender(topic string) error {
|
||||||
|
a.lock.Lock()
|
||||||
|
defer a.lock.Unlock()
|
||||||
|
|
||||||
|
connectionIsClosed := a.connection == nil || a.connection.IsClosed()
|
||||||
|
if connectionIsClosed {
|
||||||
|
connection, err := a.connectionPool.GetConnection(a.connectionPoolHandle)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get connection: %w", err)
|
||||||
|
}
|
||||||
|
a.connection = connection
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
sender, err := a.connection.NewSender(ctx, topic)
|
||||||
|
cancelFn()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("new sender: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.senderCache[topic] = sender
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements Api.Close
|
||||||
|
func (a *AmqpApi) Close(_ context.Context) error {
|
||||||
|
pkgLog.AuditLogger.Info("close audit amqp connection pool")
|
||||||
|
|
||||||
|
a.lock.Lock()
|
||||||
|
defer a.lock.Unlock()
|
||||||
|
|
||||||
|
// cached senders
|
||||||
|
var closeErrors []error
|
||||||
|
for _, session := range a.senderCache {
|
||||||
|
if err := session.Close(); err != nil {
|
||||||
|
closeErrors = append(closeErrors, fmt.Errorf("close session: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clear(a.senderCache)
|
||||||
|
|
||||||
|
// pool
|
||||||
|
if err := a.connectionPool.Close(); err != nil {
|
||||||
|
closeErrors = append(closeErrors, fmt.Errorf("close pool: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(closeErrors) > 0 {
|
||||||
|
return fmt.Errorf("close: %w", errors.Join(closeErrors...))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
463
pkg/messaging/api/amqp_test.go
Normal file
463
pkg/messaging/api/amqp_test.go
Normal file
|
|
@ -0,0 +1,463 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Azure/go-amqp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
|
internalMessaging "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/internal/messaging"
|
||||||
|
pkgMessagingCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/common"
|
||||||
|
pkgMessagingTest "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
type amqpConnMock struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *amqpConnMock) Done() <-chan struct{} {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(<-chan struct{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *amqpConnMock) NewSession(ctx context.Context, opts *amqp.SessionOptions) (internalMessaging.AmqpSession, error) {
|
||||||
|
args := m.Called(ctx, opts)
|
||||||
|
return args.Get(0).(internalMessaging.AmqpSession), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *amqpConnMock) Close() error {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ internalMessaging.AmqpConn = (*amqpConnMock)(nil)
|
||||||
|
|
||||||
|
type amqpSenderMock struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *amqpSenderMock) Send(ctx context.Context, msg *amqp.Message, opts *amqp.SendOptions) error {
|
||||||
|
return m.Called(ctx, msg, opts).Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *amqpSenderMock) Close(ctx context.Context) error {
|
||||||
|
return m.Called(ctx).Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ internalMessaging.AmqpSender = (*amqpSenderMock)(nil)
|
||||||
|
|
||||||
|
type amqpSessionMock struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *amqpSessionMock) NewSender(ctx context.Context, target string, opts *amqp.SenderOptions) (internalMessaging.AmqpSender, error) {
|
||||||
|
args := m.Called(ctx, target, opts)
|
||||||
|
return args.Get(0).(internalMessaging.AmqpSender), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *amqpSessionMock) Close(ctx context.Context) error {
|
||||||
|
args := m.Called(ctx)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ internalMessaging.AmqpSession = (*amqpSessionMock)(nil)
|
||||||
|
|
||||||
|
type connectionPoolMock struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *connectionPoolMock) Close() error {
|
||||||
|
return m.Called().Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *connectionPoolMock) NewHandle() *internalMessaging.ConnectionPoolHandle {
|
||||||
|
return m.Called().Get(0).(*internalMessaging.ConnectionPoolHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *connectionPoolMock) GetConnection(handle *internalMessaging.ConnectionPoolHandle) (*internalMessaging.AmqpConnection, error) {
|
||||||
|
return m.Called(handle).Get(0).(*internalMessaging.AmqpConnection), m.Called(handle).Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ internalMessaging.ConnectionPool = (*connectionPoolMock)(nil)
|
||||||
|
|
||||||
|
func Test_NewAmqpMessagingApi(t *testing.T) {
|
||||||
|
_, err := NewAmqpApi(
|
||||||
|
pkgMessagingCommon.AmqpConnectionPoolConfig{
|
||||||
|
Parameters: pkgMessagingCommon.AmqpConnectionConfig{BrokerUrl: "not-handled-protocol://localhost:5672"},
|
||||||
|
PoolSize: 1,
|
||||||
|
})
|
||||||
|
assert.EqualError(t, err, "new amqp connection pool: initialize connections: new connection: new internal connection: internal connect: dial: 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 := pkgMessagingTest.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(pkgMessagingCommon.AmqpConnectionPoolConfig{
|
||||||
|
Parameters: pkgMessagingCommon.AmqpConnectionConfig{BrokerUrl: solaceContainer.AmqpConnectionString},
|
||||||
|
PoolSize: 1,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = api.Send(ctx, "topic-name", []byte{}, "application/json", make(map[string]any))
|
||||||
|
assert.EqualError(t, err, "send: topic \"topic-name\" name lacks mandatory prefix \"topic://\"\nretry send: topic \"topic-name\" name lacks mandatory prefix \"topic://\"")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("send successfully", func(t *testing.T) {
|
||||||
|
defer solaceContainer.StopOnError()
|
||||||
|
|
||||||
|
// Initialize the solace queue
|
||||||
|
topicSubscriptionTopicPattern := "auditlog/>"
|
||||||
|
queueName := "send-successfully"
|
||||||
|
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||||
|
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||||
|
topicName := fmt.Sprintf("topic://auditlog/%s", "amqp-send-successfully")
|
||||||
|
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||||
|
|
||||||
|
api, err := NewDefaultAmqpApi(pkgMessagingCommon.AmqpConnectionConfig{BrokerUrl: solaceContainer.AmqpConnectionString})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
data := []byte("data")
|
||||||
|
applicationProperties := make(map[string]interface{})
|
||||||
|
applicationProperties["key"] = "value"
|
||||||
|
|
||||||
|
err = api.Send(ctx, topicName, data, "application/json", applicationProperties)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
message, err := solaceContainer.NextMessage(ctx, fmt.Sprintf("queue://%s", queueName), true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "data", string(message.Data[0]))
|
||||||
|
assert.Equal(t, topicName, *message.Properties.To)
|
||||||
|
assert.Equal(t, "application/json", *message.Properties.ContentType)
|
||||||
|
assert.Equal(t, applicationProperties, message.ApplicationProperties)
|
||||||
|
|
||||||
|
err = api.Close(ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_AmqpMessagingApi_Send_Special_Cases(t *testing.T) {
|
||||||
|
|
||||||
|
channelReceiver := func(channel chan struct{}) <-chan struct{} {
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
|
||||||
|
newActiveConnection := func() *internalMessaging.AmqpConnection {
|
||||||
|
channel := make(chan struct{})
|
||||||
|
conn := &amqpConnMock{}
|
||||||
|
conn.On("Done", mock.Anything).Return(channelReceiver(channel))
|
||||||
|
return &internalMessaging.AmqpConnection{
|
||||||
|
ConnectionName: "test",
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Conn: conn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newClosedConnection := func() *internalMessaging.AmqpConnection {
|
||||||
|
channel := make(chan struct{})
|
||||||
|
close(channel)
|
||||||
|
|
||||||
|
conn := &amqpConnMock{}
|
||||||
|
conn.On("Done", mock.Anything).Return(channelReceiver(channel))
|
||||||
|
return &internalMessaging.AmqpConnection{
|
||||||
|
ConnectionName: "test",
|
||||||
|
Lock: sync.RWMutex{},
|
||||||
|
Conn: conn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("connection nil sender nil", func(t *testing.T) {
|
||||||
|
sender := &amqpSenderMock{}
|
||||||
|
sender.On("Send", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
session := &amqpSessionMock{}
|
||||||
|
session.On("NewSender", mock.Anything, mock.Anything, mock.Anything).Return(sender, nil)
|
||||||
|
|
||||||
|
connection := newActiveConnection()
|
||||||
|
conn := connection.Conn.(*amqpConnMock)
|
||||||
|
conn.On("NewSession", mock.Anything, mock.Anything).Return(session, nil)
|
||||||
|
|
||||||
|
pool := &connectionPoolMock{}
|
||||||
|
pool.On("GetConnection", mock.Anything).Return(connection, nil)
|
||||||
|
|
||||||
|
amqpApi := &AmqpApi{
|
||||||
|
connectionPool: pool,
|
||||||
|
connectionPoolHandle: &internalMessaging.ConnectionPoolHandle{ConnectionOffset: 0},
|
||||||
|
senderCache: make(map[string]*internalMessaging.AmqpSenderSession),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := amqpApi.Send(context.Background(), "topic://some-topic", []byte("data"), "application/json", make(map[string]any))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
sender.AssertNumberOfCalls(t, "Send", 1)
|
||||||
|
session.AssertNumberOfCalls(t, "NewSender", 1)
|
||||||
|
pool.AssertNumberOfCalls(t, "GetConnection", 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("connection closed sender nil", func(t *testing.T) {
|
||||||
|
sender := &amqpSenderMock{}
|
||||||
|
sender.On("Send", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
session := &amqpSessionMock{}
|
||||||
|
session.On("NewSender", mock.Anything, mock.Anything, mock.Anything).Return(sender, nil)
|
||||||
|
|
||||||
|
connection := newActiveConnection()
|
||||||
|
conn := connection.Conn.(*amqpConnMock)
|
||||||
|
conn.On("NewSession", mock.Anything, mock.Anything).Return(session, nil)
|
||||||
|
|
||||||
|
pool := &connectionPoolMock{}
|
||||||
|
pool.On("GetConnection", mock.Anything).Return(connection, nil)
|
||||||
|
|
||||||
|
closedConnection := newClosedConnection()
|
||||||
|
closedConnMock := closedConnection.Conn.(*amqpConnMock)
|
||||||
|
amqpApi := &AmqpApi{
|
||||||
|
connection: closedConnection,
|
||||||
|
connectionPool: pool,
|
||||||
|
connectionPoolHandle: &internalMessaging.ConnectionPoolHandle{ConnectionOffset: 0},
|
||||||
|
senderCache: make(map[string]*internalMessaging.AmqpSenderSession),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := amqpApi.Send(context.Background(), "topic://some-topic", []byte("data"), "application/json", make(map[string]any))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
sender.AssertNumberOfCalls(t, "Send", 1)
|
||||||
|
session.AssertNumberOfCalls(t, "NewSender", 1)
|
||||||
|
pool.AssertNumberOfCalls(t, "GetConnection", 2)
|
||||||
|
closedConnMock.AssertNumberOfCalls(t, "Done", 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("connection nil get connection fail", func(t *testing.T) {
|
||||||
|
var connection *internalMessaging.AmqpConnection = nil
|
||||||
|
|
||||||
|
pool := &connectionPoolMock{}
|
||||||
|
pool.On("GetConnection", mock.Anything).Return(connection, errors.New("connection error"))
|
||||||
|
|
||||||
|
amqpApi := &AmqpApi{
|
||||||
|
connectionPool: pool,
|
||||||
|
connectionPoolHandle: &internalMessaging.ConnectionPoolHandle{ConnectionOffset: 0},
|
||||||
|
senderCache: make(map[string]*internalMessaging.AmqpSenderSession),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := amqpApi.Send(context.Background(), "topic://some-topic", []byte("data"), "application/json", make(map[string]any))
|
||||||
|
assert.EqualError(t, err, "get connection: connection error")
|
||||||
|
|
||||||
|
pool.AssertNumberOfCalls(t, "GetConnection", 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("connection active sender nil", func(t *testing.T) {
|
||||||
|
sender := &amqpSenderMock{}
|
||||||
|
sender.On("Send", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
session := &amqpSessionMock{}
|
||||||
|
session.On("NewSender", mock.Anything, mock.Anything, mock.Anything).Return(sender, nil)
|
||||||
|
|
||||||
|
connection := newActiveConnection()
|
||||||
|
conn := connection.Conn.(*amqpConnMock)
|
||||||
|
conn.On("NewSession", mock.Anything, mock.Anything).Return(session, nil)
|
||||||
|
|
||||||
|
amqpApi := &AmqpApi{
|
||||||
|
connection: connection,
|
||||||
|
senderCache: make(map[string]*internalMessaging.AmqpSenderSession),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := amqpApi.Send(context.Background(), "topic://some-topic", []byte("data"), "application/json", make(map[string]any))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
sender.AssertNumberOfCalls(t, "Send", 1)
|
||||||
|
session.AssertNumberOfCalls(t, "NewSender", 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("connection active new sender fail", func(t *testing.T) {
|
||||||
|
var sender *amqpSenderMock = nil
|
||||||
|
|
||||||
|
session := &amqpSessionMock{}
|
||||||
|
session.On("NewSender", mock.Anything, mock.Anything, mock.Anything).Return(sender, errors.New("new sender error"))
|
||||||
|
session.On("Close", mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
connection := newActiveConnection()
|
||||||
|
conn := connection.Conn.(*amqpConnMock)
|
||||||
|
conn.On("NewSession", mock.Anything, mock.Anything).Return(session, nil)
|
||||||
|
|
||||||
|
amqpApi := &AmqpApi{
|
||||||
|
connection: connection,
|
||||||
|
senderCache: make(map[string]*internalMessaging.AmqpSenderSession),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := amqpApi.Send(context.Background(), "topic://some-topic", []byte("data"), "application/json", make(map[string]any))
|
||||||
|
assert.EqualError(t, err, "new sender: new internal sender: new sender error")
|
||||||
|
|
||||||
|
session.AssertNumberOfCalls(t, "NewSender", 1)
|
||||||
|
session.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("connection active sender set", func(t *testing.T) {
|
||||||
|
sender := &amqpSenderMock{}
|
||||||
|
sender.On("Send", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
topic := "topic://some-topic"
|
||||||
|
amqpApi := &AmqpApi{
|
||||||
|
connection: newActiveConnection(),
|
||||||
|
senderCache: map[string]*internalMessaging.AmqpSenderSession{topic: {Sender: sender}},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := amqpApi.Send(context.Background(), topic, []byte("data"), "application/json", make(map[string]any))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
sender.AssertNumberOfCalls(t, "Send", 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("send fail", func(t *testing.T) {
|
||||||
|
sender := &amqpSenderMock{}
|
||||||
|
sender.On("Send", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("send error"))
|
||||||
|
|
||||||
|
session := &amqpSessionMock{}
|
||||||
|
session.On("NewSender", mock.Anything, mock.Anything, mock.Anything).Return(sender, nil)
|
||||||
|
|
||||||
|
topic := "topic://some-topic"
|
||||||
|
connection := newActiveConnection()
|
||||||
|
connection.Conn.(*amqpConnMock).On("NewSession", mock.Anything, mock.Anything, mock.Anything).Return(session, nil)
|
||||||
|
amqpApi := &AmqpApi{
|
||||||
|
connection: connection,
|
||||||
|
senderCache: map[string]*internalMessaging.AmqpSenderSession{topic: {Sender: sender}},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := amqpApi.Send(context.Background(), topic, []byte("data"), "application/json", make(map[string]any))
|
||||||
|
assert.EqualError(t, err, "send: send error\nretry send: send error")
|
||||||
|
|
||||||
|
sender.AssertNumberOfCalls(t, "Send", 2)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_AmqpMessagingApi_Close(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("close without cached senders", func(t *testing.T) {
|
||||||
|
pool := &connectionPoolMock{}
|
||||||
|
pool.On("Close").Return(nil)
|
||||||
|
|
||||||
|
amqpApi := &AmqpApi{
|
||||||
|
connectionPool: pool,
|
||||||
|
connectionPoolHandle: &internalMessaging.ConnectionPoolHandle{ConnectionOffset: 0},
|
||||||
|
senderCache: make(map[string]*internalMessaging.AmqpSenderSession),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := amqpApi.Close(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
pool.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("close fail without cached senders", func(t *testing.T) {
|
||||||
|
pool := &connectionPoolMock{}
|
||||||
|
pool.On("Close").Return(errors.New("close error"))
|
||||||
|
|
||||||
|
amqpApi := &AmqpApi{
|
||||||
|
connectionPool: pool,
|
||||||
|
connectionPoolHandle: &internalMessaging.ConnectionPoolHandle{ConnectionOffset: 0},
|
||||||
|
senderCache: make(map[string]*internalMessaging.AmqpSenderSession),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := amqpApi.Close(context.Background())
|
||||||
|
assert.EqualError(t, err, "close: close pool: close error")
|
||||||
|
|
||||||
|
pool.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("close with cached senders", func(t *testing.T) {
|
||||||
|
pool := &connectionPoolMock{}
|
||||||
|
pool.On("Close").Return(nil)
|
||||||
|
|
||||||
|
session := &amqpSessionMock{}
|
||||||
|
session.On("Close", mock.Anything).Return(nil)
|
||||||
|
sender := &amqpSenderMock{}
|
||||||
|
sender.On("Close", mock.Anything).Return(nil)
|
||||||
|
senderSession := &internalMessaging.AmqpSenderSession{
|
||||||
|
Session: session,
|
||||||
|
Sender: sender,
|
||||||
|
}
|
||||||
|
|
||||||
|
amqpApi := &AmqpApi{
|
||||||
|
connectionPool: pool,
|
||||||
|
connectionPoolHandle: &internalMessaging.ConnectionPoolHandle{ConnectionOffset: 0},
|
||||||
|
senderCache: map[string]*internalMessaging.AmqpSenderSession{"key": senderSession},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := amqpApi.Close(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 0, len(amqpApi.senderCache))
|
||||||
|
|
||||||
|
pool.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
session.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
sender.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("close fail with cached senders", func(t *testing.T) {
|
||||||
|
pool := &connectionPoolMock{}
|
||||||
|
pool.On("Close").Return(nil)
|
||||||
|
|
||||||
|
session := &amqpSessionMock{}
|
||||||
|
session.On("Close", mock.Anything).Return(nil)
|
||||||
|
sender := &amqpSenderMock{}
|
||||||
|
sender.On("Close", mock.Anything).Return(errors.New("close sender error"))
|
||||||
|
senderSession := &internalMessaging.AmqpSenderSession{
|
||||||
|
Session: session,
|
||||||
|
Sender: sender,
|
||||||
|
}
|
||||||
|
|
||||||
|
amqpApi := &AmqpApi{
|
||||||
|
connectionPool: pool,
|
||||||
|
connectionPoolHandle: &internalMessaging.ConnectionPoolHandle{ConnectionOffset: 0},
|
||||||
|
senderCache: map[string]*internalMessaging.AmqpSenderSession{"key": senderSession},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := amqpApi.Close(context.Background())
|
||||||
|
assert.EqualError(t, err, "close: close session: close sender error")
|
||||||
|
assert.Equal(t, 0, len(amqpApi.senderCache))
|
||||||
|
|
||||||
|
pool.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
session.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
sender.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("close fail", func(t *testing.T) {
|
||||||
|
pool := &connectionPoolMock{}
|
||||||
|
pool.On("Close").Return(errors.New("close pool error"))
|
||||||
|
|
||||||
|
session := &amqpSessionMock{}
|
||||||
|
session.On("Close", mock.Anything).Return(errors.New("close session error"))
|
||||||
|
sender := &amqpSenderMock{}
|
||||||
|
sender.On("Close", mock.Anything).Return(errors.New("close sender error"))
|
||||||
|
senderSession := &internalMessaging.AmqpSenderSession{
|
||||||
|
Session: session,
|
||||||
|
Sender: sender,
|
||||||
|
}
|
||||||
|
|
||||||
|
amqpApi := &AmqpApi{
|
||||||
|
connectionPool: pool,
|
||||||
|
connectionPoolHandle: &internalMessaging.ConnectionPoolHandle{ConnectionOffset: 0},
|
||||||
|
senderCache: map[string]*internalMessaging.AmqpSenderSession{"key": senderSession},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := amqpApi.Close(context.Background())
|
||||||
|
assert.EqualError(t, err, "close: close session: close sender error\nclose session error\nclose pool: close pool error")
|
||||||
|
assert.Equal(t, 0, len(amqpApi.senderCache))
|
||||||
|
|
||||||
|
pool.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
session.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
sender.AssertNumberOfCalls(t, "Close", 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
28
pkg/messaging/api/messaging.go
Normal file
28
pkg/messaging/api/messaging.go
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// Close the underlying connection to the messaging system.
|
||||||
|
// Parameters:
|
||||||
|
// * ctx - the context object
|
||||||
|
//
|
||||||
|
// It returns an error if the connection cannot be closed successfully
|
||||||
|
Close(ctx context.Context) error
|
||||||
|
}
|
||||||
12
pkg/messaging/common/amqp_config.go
Normal file
12
pkg/messaging/common/amqp_config.go
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
type AmqpConnectionConfig struct {
|
||||||
|
BrokerUrl string `json:"brokerUrl"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AmqpConnectionPoolConfig struct {
|
||||||
|
Parameters AmqpConnectionConfig `json:"parameters"`
|
||||||
|
PoolSize int `json:"poolSize"`
|
||||||
|
}
|
||||||
441
pkg/messaging/test/solace.go
Normal file
441
pkg/messaging/test/solace.go
Normal file
|
|
@ -0,0 +1,441 @@
|
||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Azure/go-amqp"
|
||||||
|
docker "github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/testcontainers/testcontainers-go"
|
||||||
|
"github.com/testcontainers/testcontainers-go/wait"
|
||||||
|
|
||||||
|
pkgLog "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const AmqpQueuePrefix = "queue://"
|
||||||
|
const AmqpTopicPrefix = "topic://"
|
||||||
|
const dockerImage = "schwarzit-docker.jfrog.io/solace/solace-pubsub-standard:10.8.1.241"
|
||||||
|
|
||||||
|
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: dockerImage,
|
||||||
|
ExposedPorts: []string{"5672/tcp", "8080/tcp"},
|
||||||
|
HostConfigModifier: func(config *docker.HostConfig) {
|
||||||
|
config.AutoRemove = true
|
||||||
|
config.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
|
||||||
|
}
|
||||||
|
pkgLog.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, AmqpTopicPrefix) {
|
||||||
|
name = topicName[len(AmqpTopicPrefix):]
|
||||||
|
} 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
619
proto/audit/v1/audit_event.proto
Normal file
619
proto/audit/v1/audit_event.proto
Normal file
|
|
@ -0,0 +1,619 @@
|
||||||
|
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
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
(buf.validate.field).string.pattern = ".*\\S.*"
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
(buf.validate.field).string.pattern = ".*\\S.*"
|
||||||
|
];
|
||||||
|
|
||||||
|
// The email address of the authenticated user.
|
||||||
|
// Service accounts have email addresses that can be used.
|
||||||
|
//
|
||||||
|
// Required: false
|
||||||
|
optional string principal_email = 2 [
|
||||||
|
(buf.validate.field).string.min_len = 5,
|
||||||
|
(buf.validate.field).string.max_len = 255,
|
||||||
|
(buf.validate.field).string.email = true
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
(buf.validate.field).string.pattern = ".*\\S.*"
|
||||||
|
];
|
||||||
|
|
||||||
|
// The HTTP request `Host` header value.
|
||||||
|
//
|
||||||
|
// Required: true
|
||||||
|
string host = 5 [
|
||||||
|
(buf.validate.field).required = true,
|
||||||
|
(buf.validate.field).string.min_len = 1,
|
||||||
|
(buf.validate.field).string.pattern = ".*\\S.*"
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
(buf.validate.field).string.pattern = ".*\\S.*"
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
(buf.validate.field).string.pattern = ".*\\S.*"
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
(buf.validate.field).string.pattern = ".*\\S.*"
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
(buf.validate.field).string.pattern = ".*\\S.*"
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
(buf.validate.field).string.pattern = ".*\\S.*"
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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
|
||||||
13
sonar-project.properties
Normal file
13
sonar-project.properties
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
sonar.projectKey=xx-sit-odj-sec-ident:audit-go
|
||||||
|
sonar.host.url=https://sonarqube.schwarz
|
||||||
|
sonar.projectName=audit-go
|
||||||
|
sonar.sources=.
|
||||||
|
sonar.exclusions=**/*_test.go,**/vendor/**,**/mocks/**,**/*.yml,**/gen/**, **/test/solace.go
|
||||||
|
sonar.tests=.
|
||||||
|
sonar.test.inclusions=**/*_test.go
|
||||||
|
sonar.test.exclusions=**/vendor/**,**/mocks/**
|
||||||
|
sonar.issuesReport.html.enable=true
|
||||||
|
sonar.log.level=INFO
|
||||||
|
sonar.go.coverage.reportPaths=out/cover.out
|
||||||
|
sonar.go.tests.reportPaths=out/report.json
|
||||||
|
sonar.go.golangci-lint.reportPaths=out/lint.xml
|
||||||
Loading…
Reference in a new issue