mirror of
https://dev.azure.com/schwarzit/schwarzit.stackit-public/_git/audit-go
synced 2026-02-08 00:57:24 +00:00
Add Schema and API draft
This commit is contained in:
parent
b7171c2177
commit
de5d0a8948
29 changed files with 3942 additions and 0 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -170,3 +170,6 @@ fabric.properties
|
|||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Buf
|
||||
gen
|
||||
193
README.md
Normal file
193
README.md
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
## Audit Log API
|
||||
The audit log api is a library that simplifies sending audit logs to the audit log system.
|
||||
|
||||
### Logging API
|
||||
|
||||
The audit api provides two ways to log audit events:
|
||||
|
||||
- **Direct logging**
|
||||
Logs are directly validated, serialized and sent via solace.
|
||||
It's the responsibility of the sending service to handle errors and to retry in case of an error.
|
||||
```
|
||||
Log(
|
||||
ctx context.Context,
|
||||
event *auditV1.AuditEvent,
|
||||
visibility auditV1.Visibility,
|
||||
routingIdentifier *RoutingIdentifier,
|
||||
objectIdentifier *auditV1.ObjectIdentifier,
|
||||
) error
|
||||
```
|
||||
|
||||
- **Transactional safe logging (transactional outbox pattern)**
|
||||
Functionality is split into two steps:
|
||||
- Serialization + Validation
|
||||
The `ValidateAndSerialize` method can be called to validate and serialize the event data.
|
||||
The serialized data can be stored in an outbox table / collection in
|
||||
the database.
|
||||
```
|
||||
ValidateAndSerialize(
|
||||
event *auditV1.AuditEvent,
|
||||
visibility auditV1.Visibility,
|
||||
routingIdentifier *RoutingIdentifier,
|
||||
objectIdentifier *auditV1.ObjectIdentifier,
|
||||
) (SerializedPayload, error)
|
||||
```
|
||||
|
||||
- Sending data to solace
|
||||
The `Send` method can be used to send the previously serialized data.
|
||||
It is the responsibility of the sending service to ensure that the
|
||||
method is called (repetitive) until no error is reported.
|
||||
```
|
||||
Send(
|
||||
ctx context.Context,
|
||||
routingIdentifier *RoutingIdentifier,
|
||||
serializedPayload *SerializedPayload,
|
||||
) error
|
||||
```
|
||||
|
||||
### Usage
|
||||
1. Create Solace client / acl rules
|
||||
The topic names have to be compliant to the format as specified
|
||||
[here](https://async-api.stackit.schwarz/core-platform/asyncapi.html#operation-publish-stackit-platform/t/swz/audit-log/{region}/{version}/{eventSource}/{additionalParts}).
|
||||
A solace client has to be provisioned manually with terraform (for
|
||||
[dev](https://dev.azure.com/schwarzit/schwarzit.esb/_git/QaaS-Terraform?path=/00_dev/essolske01/stackit/client_usernames.tf) /
|
||||
[qa](https://dev.azure.com/schwarzit/schwarzit.esb/_git/QaaS-Terraform?path=/02_qs/qssolske01/stackit/client_usernames.tf) /
|
||||
[prod](https://dev.azure.com/schwarzit/schwarzit.esb/_git/QaaS-Terraform?path=/03_prod/pssolske01/stackit/client_usernames.tf)).
|
||||
The permission to write to the topic needs to be specified in the terraform configuration (for
|
||||
[dev](https://dev.azure.com/schwarzit/schwarzit.esb/_git/QaaS-Terraform?path=/00_dev/essolske01/stackit/acl_profiles.tf) /
|
||||
[qa](https://dev.azure.com/schwarzit/schwarzit.esb/_git/QaaS-Terraform?path=/02_qs/qssolske01/stackit/acl_profiles.tf) /
|
||||
[prod](https://dev.azure.com/schwarzit/schwarzit.esb/_git/QaaS-Terraform?path=/03_prod/pssolske01/stackit/acl_profiles.tf)).
|
||||
|
||||
2. Instantiation of the messaging API
|
||||
The audit log api uses solace as messaging system. The actual implementation for the
|
||||
connection to the messaging system is implemented by specifying an abstraction and providing a
|
||||
concrete implementation for AMQP/Solace.
|
||||
```go
|
||||
messagingApi, err := NewAmqpMessagingApi(AmqpConfig{URL: "...", User: "", Password: ""})
|
||||
if err != nil { ... }
|
||||
```
|
||||
|
||||
3. Instantiation of the audit log api
|
||||
There will be a new Audit API in the future as audit logs will be routed dynamically
|
||||
to different data sinks in the future.
|
||||
The plan though is to keep the actual methods that (validates, serializes and) sends the log statements as stable
|
||||
as possible only replacing the API instantiation part later.
|
||||
The instantiation of the legacy api is shown below:
|
||||
```
|
||||
validator, err := protovalidate.New()
|
||||
if err != nil { ... }
|
||||
|
||||
auditApi, err := NewLegacyAuditApi(
|
||||
messagingApi,
|
||||
LegacyTopicNameConfig{TopicName: topicName},
|
||||
validator,
|
||||
)
|
||||
```
|
||||
|
||||
For each Solace topic data should be sent to, a new object instance of the legacy API is needed.
|
||||
The `messagingAPI` object can be shared across the audit api object instances.
|
||||
|
||||
4. Logging audit events
|
||||
As described in [Logging API](#logging-api) messages can be validated, serialized and sent
|
||||
through the API.
|
||||
It is important to retry as long as errors are returned to ensure that the message is really sent.
|
||||
|
||||
### Examples
|
||||
The test code can be taken as an inspiration how to use the API.
|
||||
- [api_legacy_test.go](./api_legacy_test.go)
|
||||
|
||||
### Tests
|
||||
For users of the API who integrate the library there is an additional implementation available for
|
||||
testing purpose. The `MockAuditApi` does not require a connection to a messaging system (therefore,
|
||||
does not send any data via messaging). It only serializes and validates data.
|
||||
It can be instantiated as follows:
|
||||
|
||||
```
|
||||
auditApi, err := NewMockAuditApi()
|
||||
if err != nil { ... }
|
||||
```
|
||||
|
||||
### Customization
|
||||
|
||||
#### os.Stdout logging
|
||||
|
||||
The audit log api uses `slog` to print messages on `os.Stdout` as it is part
|
||||
of the standard library (Go >= 1.21).
|
||||
|
||||
To configure slog to print json instead of plain text, the default
|
||||
logger has to be configured before using it.
|
||||
|
||||
```go
|
||||
import "log/slog"
|
||||
|
||||
func main() {
|
||||
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
|
||||
}
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
#### Build
|
||||
Before the library can be used, modified or if tests should be executed it's required to generate protobuf files
|
||||
from the schema definition.
|
||||
|
||||
##### Generate Protobuf files
|
||||
|
||||
```bash
|
||||
# Create the gen/java directory manually or rerun the buf generate command
|
||||
mkdir -p gen/java
|
||||
|
||||
cd proto
|
||||
# --include-imports is required for python code generation
|
||||
buf generate --include-imports
|
||||
```
|
||||
|
||||
##### Build library
|
||||
```bash
|
||||
go mod download && go mod tidy && go get ./... && go fmt ./... && go vet ./... && go build ./... && go test ./...
|
||||
```
|
||||
|
||||
#### 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
|
||||
|
||||
#### Testcontainers
|
||||
To run the tests **Docker** is needed as [Testcontainers](https://testcontainers.com/)
|
||||
is used to run integration tests using a solace docker container.
|
||||
|
||||
#### 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.
|
||||
|
||||
### Java and Python
|
||||
The repo currently contains additional configuration for Java and Python code generation.
|
||||
|
||||
For Python there's an example file showcasing the instantiation and validation,
|
||||
that needs to be copied to the `gen/python` directory after generating the python-files
|
||||
from the `.proto` files.
|
||||
For Python, it's additionally required to install `protovalidate` on the system as
|
||||
described [here](https://github.com/bufbuild/protovalidate-python) or by
|
||||
running `pip3 install -r requirements.txt`.
|
||||
|
||||
In the future related code and configurations for both additional languages (Java and
|
||||
Python) will be extracted into separate repositories.
|
||||
|
||||
### Open Issues
|
||||
- Finalizing messaging schema
|
||||
- Extraction of python / java configurations and code
|
||||
- Clarify if `client.go` file can be used for licence / legal reasons
|
||||
- Clean up repo (delete main.go, etc. files)
|
||||
- Finalizing API Design
|
||||
- Decision whether serialized payload should be replaced with []byte or base64 encoded string
|
||||
83
api.go
Normal file
83
api.go
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
auditV1 "audit-schema/gen/go/audit/v1"
|
||||
"context"
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// 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 patter 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 patter should be used, the ValidateAndSerialize method
|
||||
// and Send method can be called manually.
|
||||
//
|
||||
// Parameters:
|
||||
// * ctx - the context object
|
||||
// * event - the auditV1.AuditEvent
|
||||
// * visibility - route the event only internally or to the customer (not evaluated in the legacy solution)
|
||||
// * routingIdentifier - the identifier for the AMQP-Topic selection (optional)
|
||||
// * auditV1.ObjectType_OBJECT_TYPE_ORGANIZATION
|
||||
// * auditV1.ObjectType_OBJECT_TYPE_PROJECT
|
||||
// * objectIdentifier - the identifier of the object (optional - if not folder must be identical to routingIdentifier)
|
||||
// * auditV1.ObjectType_OBJECT_TYPE_ORGANIZATION
|
||||
// * auditV1.ObjectType_OBJECT_TYPE_FOLDER
|
||||
// * auditV1.ObjectType_OBJECT_TYPE_PROJECT
|
||||
//
|
||||
// It may return one of the following errors:
|
||||
/*
|
||||
- ErrEventNil - if event is nil
|
||||
- ErrObjectIdentifierVisibilityMismatch - if object identifier and visibility are not in a valid state
|
||||
- ErrRoutableIdentifierMissing - if routing identifier type and object identifier type do not match
|
||||
- ErrRoutableIdentifierTypeMismatch - if routing identifier type and object identifier types are not compatible
|
||||
- ErrUnsupportedObjectIdentifierType - if an unsupported object identifier type was provided
|
||||
- ErrUnsupportedResourceReferenceType - if an unsupported resource reference type was provided
|
||||
- protovalidate.ValidationError - if schema validation errors have been detected
|
||||
- protobuf serialization errors - if the event couldn't be serialized
|
||||
*/
|
||||
Log(ctx context.Context, event *auditV1.AuditEvent, visibility auditV1.Visibility, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier) error
|
||||
|
||||
// ValidateAndSerialize validates and serializes the event into a byte representation.
|
||||
// The result has to be sent explicitly by calling the Send method.
|
||||
ValidateAndSerialize(event *auditV1.AuditEvent, visibility auditV1.Visibility, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier) (SerializedPayload, error)
|
||||
|
||||
// Send the serialized content as byte array to the audit log system.
|
||||
Send(ctx context.Context, routingIdentifier *RoutingIdentifier, serializedPayload *SerializedPayload) error
|
||||
}
|
||||
|
||||
// ProtobufValidator is an abstraction for validators.
|
||||
// Concrete implementations are e.g. protovalidate.Validator
|
||||
type ProtobufValidator interface {
|
||||
Validate(msg proto.Message) error
|
||||
}
|
||||
|
||||
// SerializedPayload is an abstraction for serialized content
|
||||
type SerializedPayload interface {
|
||||
|
||||
// GetPayload returns the actual payload as byte array
|
||||
GetPayload() []byte
|
||||
|
||||
// GetContentType returns the content type of the payload
|
||||
GetContentType() string
|
||||
}
|
||||
|
||||
// RoutingIdentifierType is an enumeration of allowed identifier types.
|
||||
type RoutingIdentifierType int
|
||||
|
||||
// RoutingIdentifierType enumeration values.
|
||||
const (
|
||||
RoutingIdentifierTypeOrganization RoutingIdentifierType = 0
|
||||
RoutingIdentifierTypeProject RoutingIdentifierType = 1
|
||||
)
|
||||
|
||||
// RoutingIdentifier is a representation for identifiers of allowed types.
|
||||
type RoutingIdentifier struct {
|
||||
Identifier uuid.UUID
|
||||
Type RoutingIdentifierType
|
||||
}
|
||||
167
api_common.go
Normal file
167
api_common.go
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
auditV1 "audit-schema/gen/go/audit/v1"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const ContentTypeProtobuf = "application/x-protobuf"
|
||||
|
||||
// ErrEventNil indicates that the event was nil
|
||||
var ErrEventNil = errors.New("event is nil")
|
||||
|
||||
// ErrObjectIdentifierVisibilityMismatch indicates that a reference mismatch was detected.
|
||||
//
|
||||
// Valid combinations are:
|
||||
// * Visibility: Public, ObjectIdentifier: <value>
|
||||
// * Visibility: Private, ObjectIdentifier: <value | nil> -> If ObjectIdentifier is nil,
|
||||
// the ObjectReference in the message will be ObjectName_OBJECT_NAME_SYSTEM
|
||||
var ErrObjectIdentifierVisibilityMismatch = errors.New("object reference visibility mismatch")
|
||||
|
||||
// ErrRoutableIdentifierMissing indicates that a routable identifier is expected for the constellation of data.
|
||||
var ErrRoutableIdentifierMissing = errors.New("routable identifier expected")
|
||||
|
||||
// ErrRoutableIdentifierMismatch indicates that a routable identifier (uuid) does not match.
|
||||
var ErrRoutableIdentifierMismatch = errors.New("routable identifier type does not match")
|
||||
|
||||
// ErrRoutableIdentifierTypeMismatch indicates that a routable identifier type does not match the expected type.
|
||||
var ErrRoutableIdentifierTypeMismatch = errors.New("routable identifier type does not match")
|
||||
|
||||
// ErrUnsupportedObjectIdentifierType indicates that an unsupported object identifier type has been provided.
|
||||
var ErrUnsupportedObjectIdentifierType = errors.New("unsupported object identifier type")
|
||||
|
||||
// ErrUnsupportedResourceReferenceType indicates that an unsupported reference type has been provided.
|
||||
var ErrUnsupportedResourceReferenceType = errors.New("unsupported resource reference type")
|
||||
|
||||
// ErrTopicNameResolverNil states that the topic name resolve is nil
|
||||
var ErrTopicNameResolverNil = errors.New("topic name resolver nil")
|
||||
|
||||
// ErrMessagingApiNil states that the messaging api is nil
|
||||
var ErrMessagingApiNil = errors.New("messaging api nil")
|
||||
|
||||
// ErrSerializedPayloadNil states that the give serialized payload is nil
|
||||
var ErrSerializedPayloadNil = errors.New("serialized payload nil")
|
||||
|
||||
func validateAndSerializePartially(validator *ProtobufValidator, event *auditV1.AuditEvent, visibility auditV1.Visibility, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier) (*auditV1.RoutableAuditEvent, error) {
|
||||
|
||||
// Return error if the given event is nil
|
||||
if event == nil {
|
||||
return nil, ErrEventNil
|
||||
}
|
||||
|
||||
// Validate the actual event
|
||||
err := (*validator).Validate(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure that an object identifier is set if the event is public
|
||||
if objectIdentifier == nil && visibility == auditV1.Visibility_VISIBILITY_PUBLIC {
|
||||
return nil, ErrObjectIdentifierVisibilityMismatch
|
||||
}
|
||||
|
||||
// Routing identifier not set but object identifier with routable identifier
|
||||
if routingIdentifier == nil && objectIdentifier != nil &&
|
||||
visibility != auditV1.Visibility_VISIBILITY_PRIVATE {
|
||||
|
||||
return nil, ErrRoutableIdentifierMissing
|
||||
}
|
||||
|
||||
// Routing identifier type and object identifier types are not compatible
|
||||
if routingIdentifier != nil &&
|
||||
objectIdentifier != nil &&
|
||||
objectIdentifier.Type == auditV1.ObjectType_OBJECT_TYPE_FOLDER &&
|
||||
routingIdentifier.Type != RoutingIdentifierTypeOrganization {
|
||||
|
||||
return nil, ErrRoutableIdentifierTypeMismatch
|
||||
}
|
||||
|
||||
// Routing identifier type and object identifier do not match
|
||||
if routingIdentifier != nil &&
|
||||
objectIdentifier != nil &&
|
||||
objectIdentifier.Type != auditV1.ObjectType_OBJECT_TYPE_FOLDER &&
|
||||
routingIdentifier.Identifier.String() != objectIdentifier.Identifier {
|
||||
|
||||
return nil, ErrRoutableIdentifierMismatch
|
||||
}
|
||||
|
||||
// Test serialization even if the data is dropped later while 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{
|
||||
EventName: event.EventName,
|
||||
Visibility: visibility,
|
||||
Data: &auditV1.RoutableAuditEvent_UnencryptedData{UnencryptedData: &payload},
|
||||
}
|
||||
|
||||
// Set oneof protobuf fields after creation of the object
|
||||
if objectIdentifier == nil {
|
||||
routableEvent.ResourceReference = &auditV1.RoutableAuditEvent_ObjectName{ObjectName: auditV1.ObjectName_OBJECT_NAME_SYSTEM}
|
||||
} else {
|
||||
routableEvent.ResourceReference = &auditV1.RoutableAuditEvent_ObjectIdentifier{ObjectIdentifier: objectIdentifier}
|
||||
}
|
||||
|
||||
err = (*validator).Validate(&routableEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &routableEvent, nil
|
||||
}
|
||||
|
||||
func serializeToProtobufMessage(routableEvent *auditV1.RoutableAuditEvent) (SerializedPayload, error) {
|
||||
// Serialize routable event
|
||||
routableEventBytes, err := proto.Marshal(routableEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Package the routable event with information about the type in a message
|
||||
message := auditV1.ProtobufMessage{
|
||||
Value: routableEventBytes,
|
||||
ProtobufType: fmt.Sprintf("%v", routableEvent.ProtoReflect().Descriptor().FullName()),
|
||||
}
|
||||
|
||||
// Serialize message
|
||||
messageBytes, err := proto.Marshal(&message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &routablePayload{
|
||||
payload: messageBytes,
|
||||
contentType: ContentTypeProtobuf,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Send implements AuditApi.Send
|
||||
func send(topicNameResolver *TopicNameResolver, messagingApi *MessagingApi, ctx context.Context, routingIdentifier *RoutingIdentifier, serializedPayload *SerializedPayload) error {
|
||||
|
||||
if topicNameResolver == nil {
|
||||
return ErrTopicNameResolverNil
|
||||
}
|
||||
if messagingApi == nil {
|
||||
return ErrMessagingApiNil
|
||||
}
|
||||
if serializedPayload == nil {
|
||||
return ErrSerializedPayloadNil
|
||||
}
|
||||
|
||||
topic, err := (*topicNameResolver).Resolve(routingIdentifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return (*messagingApi).Send(ctx, topic, (*serializedPayload).GetPayload(), (*serializedPayload).GetContentType())
|
||||
}
|
||||
227
api_common_test.go
Normal file
227
api_common_test.go
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
auditV1 "audit-schema/gen/go/audit/v1"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/bufbuild/protovalidate-go"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type ProtobufValidatorMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *ProtobufValidatorMock) Validate(msg proto.Message) error {
|
||||
args := m.Called(msg)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
type TopicNameResolverMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *TopicNameResolverMock) Resolve(routingIdentifier *RoutingIdentifier) (string, error) {
|
||||
args := m.Called(routingIdentifier)
|
||||
return args.String(0), args.Error(1)
|
||||
}
|
||||
|
||||
func NewValidator(t *testing.T) ProtobufValidator {
|
||||
validator, err := protovalidate.New()
|
||||
var protoValidator ProtobufValidator = validator
|
||||
assert.NoError(t, err)
|
||||
|
||||
return protoValidator
|
||||
}
|
||||
|
||||
func Test_ValidateAndSerializePartially_EventNil(t *testing.T) {
|
||||
validator := NewValidator(t)
|
||||
|
||||
_, err := validateAndSerializePartially(&validator, nil, auditV1.Visibility_VISIBILITY_PUBLIC, nil, nil)
|
||||
assert.ErrorIs(t, err, ErrEventNil)
|
||||
}
|
||||
|
||||
func Test_ValidateAndSerializePartially_AuditEventValidationFailed(t *testing.T) {
|
||||
validator := NewValidator(t)
|
||||
|
||||
event, routingIdentifier, objectIdentifier := NewOrganizationAuditEvent(nil)
|
||||
event.EventName = ""
|
||||
|
||||
_, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, routingIdentifier, objectIdentifier)
|
||||
assert.EqualError(t, err, "validation error:\n - event_name: value is required [required]")
|
||||
}
|
||||
|
||||
func Test_ValidateAndSerializePartially_RoutableEventValidationFailed(t *testing.T) {
|
||||
validator := NewValidator(t)
|
||||
|
||||
event, routingIdentifier, objectIdentifier := NewOrganizationAuditEvent(nil)
|
||||
|
||||
_, err := validateAndSerializePartially(&validator, event, 3, routingIdentifier, objectIdentifier)
|
||||
assert.EqualError(t, err, "validation error:\n - visibility: value must be one of the defined enum values [enum.defined_only]")
|
||||
}
|
||||
|
||||
func Test_ValidateAndSerializePartially_CheckVisibility(t *testing.T) {
|
||||
validator := NewValidator(t)
|
||||
|
||||
event, routingIdentifier, objectIdentifier := NewOrganizationAuditEvent(nil)
|
||||
|
||||
t.Run("Visibility public - object identifier nil - routing identifier nil", func(t *testing.T) {
|
||||
_, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, nil, nil)
|
||||
assert.ErrorIs(t, err, ErrObjectIdentifierVisibilityMismatch)
|
||||
})
|
||||
|
||||
t.Run("Visibility public - object identifier nil - routing identifier set", func(t *testing.T) {
|
||||
_, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, routingIdentifier, nil)
|
||||
assert.ErrorIs(t, err, ErrObjectIdentifierVisibilityMismatch)
|
||||
})
|
||||
|
||||
t.Run("Visibility public - object identifier set - routing identifier nil", func(t *testing.T) {
|
||||
_, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, nil, objectIdentifier)
|
||||
assert.ErrorIs(t, err, ErrRoutableIdentifierMissing)
|
||||
})
|
||||
|
||||
t.Run("Visibility public - object identifier set - routing identifier set", func(t *testing.T) {
|
||||
routableEvent, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, routingIdentifier, objectIdentifier)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, routableEvent)
|
||||
})
|
||||
|
||||
t.Run("Visibility private - object identifier nil - routing identifier nil", func(t *testing.T) {
|
||||
routableEvent, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, routableEvent)
|
||||
})
|
||||
|
||||
t.Run("Visibility private - object identifier nil - routing identifier set", func(t *testing.T) {
|
||||
routableEvent, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, routingIdentifier, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, routableEvent)
|
||||
})
|
||||
|
||||
t.Run("Visibility private - object identifier set - routing identifier nil", func(t *testing.T) {
|
||||
routableEvent, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, nil, objectIdentifier)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, routableEvent)
|
||||
})
|
||||
|
||||
t.Run("Visibility private - object identifier set - routing identifier set", func(t *testing.T) {
|
||||
routableEvent, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, routingIdentifier, objectIdentifier)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, routableEvent)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_ValidateAndSerializePartially_IdentifierTypeMismatch(t *testing.T) {
|
||||
validator := NewValidator(t)
|
||||
|
||||
event, routingIdentifier, objectIdentifier := NewFolderAuditEvent(nil)
|
||||
routingIdentifier.Type = RoutingIdentifierTypeProject
|
||||
|
||||
_, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, routingIdentifier, objectIdentifier)
|
||||
assert.ErrorIs(t, err, ErrRoutableIdentifierTypeMismatch)
|
||||
}
|
||||
|
||||
func Test_ValidateAndSerializePartially_IdentifierMismatch(t *testing.T) {
|
||||
validator := NewValidator(t)
|
||||
|
||||
event, routingIdentifier, objectIdentifier := NewProjectAuditEvent(nil)
|
||||
routingIdentifier.Identifier = uuid.New()
|
||||
|
||||
_, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, routingIdentifier, objectIdentifier)
|
||||
assert.ErrorIs(t, err, ErrRoutableIdentifierMismatch)
|
||||
}
|
||||
|
||||
func Test_ValidateAndSerializePartially_SystemEvent(t *testing.T) {
|
||||
validator := NewValidator(t)
|
||||
|
||||
event := NewSystemAuditEvent(nil)
|
||||
|
||||
routableEvent, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
switch reference := routableEvent.ResourceReference.(type) {
|
||||
case *auditV1.RoutableAuditEvent_ObjectName:
|
||||
assert.Equal(t, auditV1.ObjectName_OBJECT_NAME_SYSTEM, reference.ObjectName)
|
||||
default:
|
||||
assert.Fail(t, "unexpected resource reference")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SerializeToProtobufMessage(t *testing.T) {
|
||||
validator := NewValidator(t)
|
||||
|
||||
// Create test data
|
||||
event, identifier, objectIdentifier := NewOrganizationAuditEventWithDetails()
|
||||
|
||||
// Serialize to routable event
|
||||
routableEvent, err := validateAndSerializePartially(&validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, identifier, objectIdentifier)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Serialize to protobuf message
|
||||
serializedPayload, err := serializeToProtobufMessage(routableEvent)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, serializedPayload.GetContentType(), ContentTypeProtobuf)
|
||||
|
||||
// Deserialize
|
||||
var deserializedEvent auditV1.ProtobufMessage
|
||||
assert.NoError(t, proto.Unmarshal(serializedPayload.GetPayload(), &deserializedEvent))
|
||||
|
||||
expectedProtobufType := fmt.Sprintf("%v", routableEvent.ProtoReflect().Descriptor().FullName())
|
||||
assert.Equal(t, expectedProtobufType, deserializedEvent.ProtobufType)
|
||||
|
||||
var deserializedRoutableEvent auditV1.RoutableAuditEvent
|
||||
assert.NoError(t, proto.Unmarshal(deserializedEvent.Value, &deserializedRoutableEvent))
|
||||
assert.True(t, proto.Equal(routableEvent, &deserializedRoutableEvent))
|
||||
}
|
||||
|
||||
func Test_Send_TopicNameResolverNil(t *testing.T) {
|
||||
err := send(nil, nil, context.Background(), nil, nil)
|
||||
assert.ErrorIs(t, err, ErrTopicNameResolverNil)
|
||||
}
|
||||
|
||||
func Test_Send_TopicNameResolutionError(t *testing.T) {
|
||||
expectedError := errors.New("expected error")
|
||||
|
||||
topicNameResolverMock := TopicNameResolverMock{}
|
||||
topicNameResolverMock.On("Resolve", mock.Anything).Return("topic", expectedError)
|
||||
var topicNameResolver TopicNameResolver = &topicNameResolverMock
|
||||
|
||||
var serializedPayload SerializedPayload = &routablePayload{}
|
||||
|
||||
var messagingApi MessagingApi = &AmqpMessagingApi{}
|
||||
err := send(&topicNameResolver, &messagingApi, context.Background(), nil, &serializedPayload)
|
||||
assert.ErrorIs(t, err, expectedError)
|
||||
}
|
||||
|
||||
func Test_Send_MessagingApiNil(t *testing.T) {
|
||||
var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: "test"}
|
||||
err := send(&topicNameResolver, nil, context.Background(), nil, nil)
|
||||
assert.ErrorIs(t, err, ErrMessagingApiNil)
|
||||
}
|
||||
|
||||
func Test_Send_SerializedPayloadNil(t *testing.T) {
|
||||
var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: "test"}
|
||||
var messagingApi MessagingApi = &AmqpMessagingApi{}
|
||||
err := send(&topicNameResolver, &messagingApi, context.Background(), nil, nil)
|
||||
assert.ErrorIs(t, err, ErrSerializedPayloadNil)
|
||||
}
|
||||
|
||||
func Test_Send(t *testing.T) {
|
||||
topicNameResolverMock := TopicNameResolverMock{}
|
||||
topicNameResolverMock.On("Resolve", mock.Anything).Return("topic", nil)
|
||||
var topicNameResolver TopicNameResolver = &topicNameResolverMock
|
||||
|
||||
var serializedPayload SerializedPayload = &routablePayload{}
|
||||
|
||||
messagingApiMock := MessagingApiMock{}
|
||||
messagingApiMock.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
var messagingApi MessagingApi = &messagingApiMock
|
||||
|
||||
assert.NoError(t, send(&topicNameResolver, &messagingApi, context.Background(), nil, &serializedPayload))
|
||||
assert.True(t, messagingApiMock.AssertNumberOfCalls(t, "Send", 1))
|
||||
}
|
||||
346
api_legacy.go
Normal file
346
api_legacy.go
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
auditV1 "audit-schema/gen/go/audit/v1"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"time"
|
||||
)
|
||||
|
||||
// legacyPayload implements SerializedPayload.
|
||||
type legacyPayload struct {
|
||||
payload []byte
|
||||
}
|
||||
|
||||
// GetPayload implements SerializedPayload.GetPayload.
|
||||
func (p *legacyPayload) GetPayload() []byte {
|
||||
return p.payload
|
||||
}
|
||||
|
||||
// GetContentType implements SerializedPayload.GetContentType.
|
||||
func (p *legacyPayload) GetContentType() string {
|
||||
return "application/json"
|
||||
}
|
||||
|
||||
// LegacyTopicNameResolver implements TopicNameResolver.
|
||||
// A hard-coded topic name is used, routing identifiers are ignored.
|
||||
type LegacyTopicNameResolver struct {
|
||||
topicName string
|
||||
}
|
||||
|
||||
// Resolve implements TopicNameResolver.Resolve
|
||||
func (r *LegacyTopicNameResolver) Resolve(*RoutingIdentifier) (string, error) {
|
||||
return r.topicName, nil
|
||||
}
|
||||
|
||||
// LegacyTopicNameConfig provides topic name information required for the topic name resolution.
|
||||
type LegacyTopicNameConfig struct {
|
||||
TopicName string
|
||||
}
|
||||
|
||||
// LegacyAuditApi is an implementation of AuditApi to send events to the legacy audit log system.
|
||||
//
|
||||
// Note: The implementation will be deprecated and replaced with the "routableAuditApi" once the new audit log routing is implemented
|
||||
type LegacyAuditApi struct {
|
||||
messagingApi *MessagingApi
|
||||
topicNameResolver *TopicNameResolver
|
||||
validator *ProtobufValidator
|
||||
}
|
||||
|
||||
// NewLegacyAuditApi can be used to initialize the audit log api with LegacyAuditLogConnectionDetails.
|
||||
//
|
||||
// Note: The NewLegacyAuditApi method will be deprecated and replaced with "newRoutableAuditApi" once the new audit log routing is implemented
|
||||
func NewLegacyAuditApi(messagingApi *MessagingApi, topicNameConfig LegacyTopicNameConfig, validator ProtobufValidator) (*AuditApi, error) {
|
||||
|
||||
if messagingApi == nil {
|
||||
return nil, errors.New("messaging api nil")
|
||||
}
|
||||
|
||||
// Topic resolver
|
||||
var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: topicNameConfig.TopicName}
|
||||
|
||||
// Audit api
|
||||
var auditApi AuditApi = &LegacyAuditApi{
|
||||
messagingApi: messagingApi,
|
||||
topicNameResolver: &topicNameResolver,
|
||||
validator: &validator,
|
||||
}
|
||||
|
||||
return &auditApi, nil
|
||||
}
|
||||
|
||||
// Log implements AuditApi.Log
|
||||
func (a *LegacyAuditApi) Log(ctx context.Context, event *auditV1.AuditEvent, visibility auditV1.Visibility, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier) error {
|
||||
|
||||
serializedPayload, err := a.ValidateAndSerialize(event, visibility, routingIdentifier, objectIdentifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.Send(ctx, routingIdentifier, &serializedPayload)
|
||||
}
|
||||
|
||||
// ValidateAndSerialize implements AuditApi.ValidateAndSerialize.
|
||||
// It serializes the event into the byte representation of the legacy audit log system.
|
||||
func (a *LegacyAuditApi) ValidateAndSerialize(event *auditV1.AuditEvent, visibility auditV1.Visibility, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier) (SerializedPayload, error) {
|
||||
routableEvent, err := validateAndSerializePartially(a.validator, event, visibility, routingIdentifier, objectIdentifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Do nothing with the serialized data in the legacy solution
|
||||
_, err = proto.Marshal(routableEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert attributes
|
||||
legacyBytes, err := a.convertAndSerializeIntoLegacyFormat(event, routableEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &legacyPayload{payload: legacyBytes}, nil
|
||||
}
|
||||
|
||||
// Send implements AuditApi.Send
|
||||
func (a *LegacyAuditApi) Send(ctx context.Context, routingIdentifier *RoutingIdentifier, serializedPayload *SerializedPayload) error {
|
||||
return send(a.topicNameResolver, a.messagingApi, ctx, routingIdentifier, serializedPayload)
|
||||
}
|
||||
|
||||
// convertAndSerializeIntoLegacyFormat converts the protobuf events into the json serialized legacy audit log format
|
||||
func (a *LegacyAuditApi) convertAndSerializeIntoLegacyFormat(
|
||||
event *auditV1.AuditEvent,
|
||||
routableEvent *auditV1.RoutableAuditEvent,
|
||||
) ([]byte, error) {
|
||||
|
||||
// Source IP & User agent
|
||||
var sourceIpAddress string
|
||||
var userAgent string
|
||||
if event.Request == nil {
|
||||
sourceIpAddress = "0.0.0.0"
|
||||
userAgent = "none"
|
||||
} else {
|
||||
sourceIpAddress = event.Request.GetSourceIpAddress()
|
||||
userAgent = event.Request.GetUserAgent()
|
||||
}
|
||||
|
||||
// Principals
|
||||
var serviceAccountDelegationInfo *LegacyAuditEventServiceAccountDelegationInfo = nil
|
||||
if len(event.Principals) > 0 {
|
||||
var principals []LegacyAuditEventPrincipal
|
||||
for _, principal := range event.Principals {
|
||||
if principal != nil {
|
||||
p := LegacyAuditEventPrincipal{
|
||||
Id: principal.Id,
|
||||
Email: principal.Email,
|
||||
}
|
||||
principals = append(principals, p)
|
||||
}
|
||||
}
|
||||
serviceAccountDelegationInfo = &LegacyAuditEventServiceAccountDelegationInfo{Principals: principals}
|
||||
}
|
||||
|
||||
// Request
|
||||
var request LegacyAuditEventRequest
|
||||
if event.Request == nil {
|
||||
request = LegacyAuditEventRequest{
|
||||
Endpoint: "none",
|
||||
}
|
||||
} else {
|
||||
var parameters map[string]interface{} = nil
|
||||
if event.Request.Parameters != nil {
|
||||
parameters = event.Request.Parameters.AsMap()
|
||||
}
|
||||
|
||||
var body map[string]interface{} = nil
|
||||
if event.Request.Body != nil {
|
||||
body = event.Request.Body.AsMap()
|
||||
}
|
||||
var headers map[string]interface{} = nil
|
||||
if event.Request.Headers != nil {
|
||||
headers = map[string]interface{}{}
|
||||
for _, header := range event.Request.Headers {
|
||||
if header != nil {
|
||||
headers[header.Key] = header.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
request = LegacyAuditEventRequest{
|
||||
Endpoint: event.Request.Endpoint,
|
||||
Parameters: ¶meters,
|
||||
Body: &body,
|
||||
Headers: &headers,
|
||||
}
|
||||
}
|
||||
|
||||
// Context and event type
|
||||
var messageContext *LegacyAuditEventContext
|
||||
var eventType string
|
||||
switch ref := routableEvent.GetResourceReference().(type) {
|
||||
case *auditV1.RoutableAuditEvent_ObjectIdentifier:
|
||||
eventType = "ADMIN_ACTIVITY"
|
||||
if ref.ObjectIdentifier.Type == auditV1.ObjectType_OBJECT_TYPE_ORGANIZATION {
|
||||
messageContext = &LegacyAuditEventContext{
|
||||
OrganizationId: nil,
|
||||
FolderId: nil,
|
||||
ProjectId: nil,
|
||||
}
|
||||
|
||||
} else if ref.ObjectIdentifier.Type == auditV1.ObjectType_OBJECT_TYPE_FOLDER {
|
||||
messageContext = &LegacyAuditEventContext{
|
||||
OrganizationId: nil,
|
||||
FolderId: nil,
|
||||
ProjectId: nil,
|
||||
}
|
||||
} else if ref.ObjectIdentifier.Type == auditV1.ObjectType_OBJECT_TYPE_PROJECT {
|
||||
messageContext = &LegacyAuditEventContext{
|
||||
OrganizationId: nil,
|
||||
FolderId: nil,
|
||||
ProjectId: nil,
|
||||
}
|
||||
} else {
|
||||
return nil, ErrUnsupportedObjectIdentifierType
|
||||
}
|
||||
break
|
||||
case *auditV1.RoutableAuditEvent_ObjectName:
|
||||
eventType = "SYSTEM_EVENT"
|
||||
messageContext = nil
|
||||
break
|
||||
default:
|
||||
return nil, ErrUnsupportedResourceReferenceType
|
||||
}
|
||||
|
||||
// Details
|
||||
var details map[string]interface{} = nil
|
||||
if event.Details != nil {
|
||||
details = event.Details.AsMap()
|
||||
}
|
||||
|
||||
// Result
|
||||
var result map[string]interface{} = nil
|
||||
if event.Result != nil {
|
||||
result = event.Result.AsMap()
|
||||
}
|
||||
|
||||
// Instantiate the legacy event - missing values are filled with defaults
|
||||
legacyAuditEvent := LegacyAuditEvent{
|
||||
Severity: "INFO",
|
||||
Visibility: routableEvent.Visibility.String(),
|
||||
EventType: eventType,
|
||||
EventTimeStamp: event.EventTimeStamp.AsTime(),
|
||||
EventName: event.EventName,
|
||||
SourceIpAddress: sourceIpAddress,
|
||||
UserAgent: userAgent,
|
||||
Initiator: LegacyAuditEventPrincipal{
|
||||
Id: event.Initiator.Id,
|
||||
Email: event.Initiator.Email,
|
||||
},
|
||||
ServiceAccountDelegationInfo: serviceAccountDelegationInfo,
|
||||
Request: request,
|
||||
Context: messageContext,
|
||||
ResourceId: event.ResourceId,
|
||||
ResourceName: event.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"`
|
||||
ServiceAccountDelegationInfo *LegacyAuditEventServiceAccountDelegationInfo `json:"serviceAccountDelegationInfo"`
|
||||
Request LegacyAuditEventRequest `json:"request"`
|
||||
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"`
|
||||
}
|
||||
486
api_legacy_test.go
Normal file
486
api_legacy_test.go
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
auditV1 "audit-schema/gen/go/audit/v1"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/Azure/go-amqp"
|
||||
"github.com/bufbuild/protovalidate-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"log/slog"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLegacyAuditApi(t *testing.T) {
|
||||
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
|
||||
|
||||
// Specify test timeout
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancelFn()
|
||||
|
||||
// Start solace docker container
|
||||
solaceContainer, err := NewSolaceContainer(context.Background())
|
||||
assert.NoError(t, err)
|
||||
defer solaceContainer.Stop()
|
||||
|
||||
// Instantiate the messaging api
|
||||
messagingApi, err := NewAmqpMessagingApi(AmqpConfig{URL: solaceContainer.AmqpConnectionString})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Validator
|
||||
validator, err := protovalidate.New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
topicSubscriptionTopicPattern := "audit-log/>"
|
||||
|
||||
// Check logging of organization events
|
||||
t.Run("Log public organization event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
slog.Info("test abc")
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "org-event-public-legacy"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/organization-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewLegacyAuditApi(
|
||||
messagingApi,
|
||||
LegacyTopicNameConfig{TopicName: topicName},
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, routingIdentifier, objectIdentifier := NewOrganizationAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
assert.NoError(t, (*auditApi).Log(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
routingIdentifier,
|
||||
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://audit-log/eu01/v1/resource-manager/organization-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewLegacyAuditApi(
|
||||
messagingApi,
|
||||
LegacyTopicNameConfig{TopicName: topicName},
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, routingIdentifier, objectIdentifier := NewOrganizationAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
assert.NoError(t, (*auditApi).Log(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
routingIdentifier,
|
||||
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://audit-log/eu01/v1/resource-manager/folder-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewLegacyAuditApi(
|
||||
messagingApi,
|
||||
LegacyTopicNameConfig{TopicName: topicName},
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, routingIdentifier, objectIdentifier := NewFolderAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
assert.NoError(t, (*auditApi).Log(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
routingIdentifier,
|
||||
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://audit-log/eu01/v1/resource-manager/folder-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewLegacyAuditApi(
|
||||
messagingApi,
|
||||
LegacyTopicNameConfig{TopicName: topicName},
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, routingIdentifier, objectIdentifier := NewFolderAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
assert.NoError(t, (*auditApi).Log(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
routingIdentifier,
|
||||
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://audit-log/eu01/v1/resource-manager/project-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewLegacyAuditApi(
|
||||
messagingApi,
|
||||
LegacyTopicNameConfig{TopicName: topicName},
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, routingIdentifier, objectIdentifier := NewProjectAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
assert.NoError(t, (*auditApi).Log(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
routingIdentifier,
|
||||
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://audit-log/eu01/v1/resource-manager/project-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewLegacyAuditApi(
|
||||
messagingApi,
|
||||
LegacyTopicNameConfig{TopicName: topicName},
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, routingIdentifier, objectIdentifier := NewProjectAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
assert.NoError(t, (*auditApi).Log(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
routingIdentifier,
|
||||
objectIdentifier,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentMessage(t, topicName, message, event)
|
||||
})
|
||||
|
||||
// Check logging of system events
|
||||
t.Run("Log private system event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
queueName := "system-event-private"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/system-changed"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewLegacyAuditApi(
|
||||
messagingApi,
|
||||
LegacyTopicNameConfig{TopicName: topicName},
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event := NewSystemAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
assert.NoError(t,
|
||||
(*auditApi).Log(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
nil,
|
||||
nil,
|
||||
))
|
||||
|
||||
// Receive the event from solace
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check topic name
|
||||
assert.Equal(t, topicName, *message.Properties.To)
|
||||
|
||||
// Check topic name
|
||||
assert.Equal(t, topicName, *message.Properties.To)
|
||||
|
||||
// Check deserialized message
|
||||
var auditEvent LegacyAuditEvent
|
||||
assert.NoError(t, json.Unmarshal(message.Data[0], &auditEvent))
|
||||
|
||||
assert.Equal(t, event.EventName, auditEvent.EventName)
|
||||
assert.Equal(t, event.EventTimeStamp.AsTime(), auditEvent.EventTimeStamp)
|
||||
assert.Equal(t, event.Initiator.Id, auditEvent.Initiator.Id)
|
||||
assert.Equal(t, "SYSTEM_EVENT", auditEvent.EventType)
|
||||
assert.Equal(t, "INFO", auditEvent.Severity)
|
||||
assert.Equal(t, "none", auditEvent.Request.Endpoint)
|
||||
assert.Equal(t, "0.0.0.0", auditEvent.SourceIpAddress)
|
||||
assert.Equal(t, "none", auditEvent.UserAgent)
|
||||
})
|
||||
|
||||
t.Run("Log event with details", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Create the queue and topic subscription in solace
|
||||
queueName := "org-event-with-details-legacy"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
|
||||
topicName := "topic://audit-log/eu01/v1/resource-manager/organization-created"
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
// Instantiate audit api
|
||||
auditApi, err := NewLegacyAuditApi(
|
||||
messagingApi,
|
||||
LegacyTopicNameConfig{TopicName: topicName},
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, routingIdentifier, objectIdentifier := NewOrganizationAuditEventWithDetails()
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
assert.NoError(t, (*auditApi).Log(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
routingIdentifier,
|
||||
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.AuditEvent) {
|
||||
// Check topic name
|
||||
assert.Equal(t, topicName, *message.Properties.To)
|
||||
|
||||
// Check deserialized message
|
||||
var auditEvent LegacyAuditEvent
|
||||
assert.NoError(t, json.Unmarshal(message.Data[0], &auditEvent))
|
||||
|
||||
assert.Equal(t, event.EventName, auditEvent.EventName)
|
||||
assert.Equal(t, event.EventTimeStamp.AsTime(), auditEvent.EventTimeStamp)
|
||||
assert.Equal(t, event.Initiator.Id, auditEvent.Initiator.Id)
|
||||
assert.Equal(t, "ADMIN_ACTIVITY", auditEvent.EventType)
|
||||
assert.Equal(t, "INFO", auditEvent.Severity)
|
||||
assert.Equal(t, "none", auditEvent.Request.Endpoint)
|
||||
assert.Equal(t, "0.0.0.0", auditEvent.SourceIpAddress)
|
||||
assert.Equal(t, "none", auditEvent.UserAgent)
|
||||
}
|
||||
|
||||
func validateSentMessageWithDetails(t *testing.T, topicName string, message *amqp.Message, event *auditV1.AuditEvent) {
|
||||
// Check topic name
|
||||
assert.Equal(t, topicName, *message.Properties.To)
|
||||
|
||||
// Check deserialized message
|
||||
var auditEvent LegacyAuditEvent
|
||||
assert.NoError(t, json.Unmarshal(message.Data[0], &auditEvent))
|
||||
|
||||
assert.Equal(t, event.EventName, auditEvent.EventName)
|
||||
assert.Equal(t, event.EventTimeStamp.AsTime(), auditEvent.EventTimeStamp)
|
||||
assert.Equal(t, event.Initiator.Id, auditEvent.Initiator.Id)
|
||||
assert.Equal(t, "ADMIN_ACTIVITY", auditEvent.EventType)
|
||||
assert.Equal(t, "INFO", auditEvent.Severity)
|
||||
assert.Equal(t, event.Request.Endpoint, auditEvent.Request.Endpoint)
|
||||
assert.Equal(t, event.Request.SourceIpAddress, auditEvent.SourceIpAddress)
|
||||
assert.Equal(t, *event.Request.UserAgent, auditEvent.UserAgent)
|
||||
assert.Equal(t, event.Request.Body.AsMap(), *auditEvent.Request.Body)
|
||||
assert.Equal(t, event.Request.Parameters.AsMap(), *auditEvent.Request.Parameters)
|
||||
assert.Equal(t, event.Details.AsMap(), *auditEvent.Details)
|
||||
assert.Equal(t, event.Result.AsMap(), *auditEvent.Result)
|
||||
for _, header := range event.Request.Headers {
|
||||
assert.Equal(t, header.Value, (*auditEvent.Request.Headers)[header.Key])
|
||||
}
|
||||
|
||||
for idx, _ := range event.Principals {
|
||||
assert.Equal(t, event.Principals[idx].Id, auditEvent.ServiceAccountDelegationInfo.Principals[idx].Id)
|
||||
assert.Equal(t, event.Principals[idx].Email, auditEvent.ServiceAccountDelegationInfo.Principals[idx].Email)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyAuditApi_NewLegacyAuditApi_MessagingApiNil(t *testing.T) {
|
||||
auditApi, err := NewLegacyAuditApi(nil, LegacyTopicNameConfig{}, nil)
|
||||
assert.Nil(t, auditApi)
|
||||
assert.EqualError(t, err, "messaging api nil")
|
||||
}
|
||||
|
||||
func TestLegacyAuditApi_ValidateAndSerialize_ValidationFailed(t *testing.T) {
|
||||
expectedError := errors.New("expected error")
|
||||
|
||||
validator := &ProtobufValidatorMock{}
|
||||
validator.On("Validate", mock.Anything).Return(expectedError)
|
||||
var protobufValidator ProtobufValidator = validator
|
||||
|
||||
auditApi := LegacyAuditApi{validator: &protobufValidator}
|
||||
|
||||
event := NewSystemAuditEvent(nil)
|
||||
_, err := auditApi.ValidateAndSerialize(event, auditV1.Visibility_VISIBILITY_PUBLIC, nil, nil)
|
||||
assert.ErrorIs(t, err, expectedError)
|
||||
}
|
||||
|
||||
func TestLegacyAuditApi_Log_ValidationFailed(t *testing.T) {
|
||||
expectedError := errors.New("expected error")
|
||||
|
||||
validator := &ProtobufValidatorMock{}
|
||||
validator.On("Validate", mock.Anything).Return(expectedError)
|
||||
var protobufValidator ProtobufValidator = validator
|
||||
|
||||
auditApi := LegacyAuditApi{validator: &protobufValidator}
|
||||
|
||||
event := NewSystemAuditEvent(nil)
|
||||
err := auditApi.Log(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, nil, nil)
|
||||
assert.ErrorIs(t, err, expectedError)
|
||||
}
|
||||
|
||||
func TestLegacyAuditApi_Log_NilEvent(t *testing.T) {
|
||||
auditApi := LegacyAuditApi{}
|
||||
err := auditApi.Log(context.Background(), nil, auditV1.Visibility_VISIBILITY_PUBLIC, nil, nil)
|
||||
assert.ErrorIs(t, err, ErrEventNil)
|
||||
}
|
||||
|
||||
func TestLegacyAuditApi_ConvertAndSerializeIntoLegacyFormatInvalidObjectIdentifierType(t *testing.T) {
|
||||
customization := func(event *auditV1.AuditEvent,
|
||||
routingIdentifier *RoutingIdentifier,
|
||||
objectIdentifier *auditV1.ObjectIdentifier) {
|
||||
objectIdentifier.Type = 4
|
||||
}
|
||||
event, identifier, objectIdentifier := NewProjectAuditEvent(&customization)
|
||||
|
||||
validator := &ProtobufValidatorMock{}
|
||||
validator.On("Validate", mock.Anything).Return(nil)
|
||||
var protobufValidator ProtobufValidator = validator
|
||||
|
||||
auditApi := LegacyAuditApi{validator: &protobufValidator}
|
||||
_, err := auditApi.ValidateAndSerialize(event, auditV1.Visibility_VISIBILITY_PUBLIC, identifier, objectIdentifier)
|
||||
assert.ErrorIs(t, err, ErrUnsupportedObjectIdentifierType)
|
||||
}
|
||||
|
||||
func TestLegacyAuditApi_ConvertAndSerializeIntoLegacyFormat_NoResourceReference(t *testing.T) {
|
||||
event, _, _ := NewProjectAuditEvent(nil)
|
||||
routableEvent := auditV1.RoutableAuditEvent{
|
||||
EventName: event.EventName,
|
||||
Visibility: auditV1.Visibility_VISIBILITY_PUBLIC,
|
||||
ResourceReference: nil,
|
||||
Data: nil,
|
||||
}
|
||||
|
||||
auditApi := LegacyAuditApi{}
|
||||
_, err := auditApi.convertAndSerializeIntoLegacyFormat(event, &routableEvent)
|
||||
assert.ErrorIs(t, err, ErrUnsupportedResourceReferenceType)
|
||||
}
|
||||
51
api_mock.go
Normal file
51
api_mock.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
auditV1 "audit-schema/gen/go/audit/v1"
|
||||
"context"
|
||||
"github.com/bufbuild/protovalidate-go"
|
||||
)
|
||||
|
||||
// MockAuditApi is an implementation of AuditApi that does nothing and has no dependency to external systems.
|
||||
type MockAuditApi struct {
|
||||
validator *ProtobufValidator
|
||||
}
|
||||
|
||||
func NewMockAuditApi() (*AuditApi, error) {
|
||||
validator, err := protovalidate.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var protobufValidator ProtobufValidator = validator
|
||||
var auditApi AuditApi = &MockAuditApi{validator: &protobufValidator}
|
||||
return &auditApi, nil
|
||||
}
|
||||
|
||||
// Log implements AuditApi.Log.
|
||||
// Validates and serializes the event but doesn't send it.
|
||||
func (a *MockAuditApi) Log(
|
||||
_ context.Context,
|
||||
event *auditV1.AuditEvent,
|
||||
visibility auditV1.Visibility,
|
||||
routingIdentifier *RoutingIdentifier,
|
||||
objectIdentifier *auditV1.ObjectIdentifier,
|
||||
) error {
|
||||
|
||||
_, err := a.ValidateAndSerialize(event, visibility, routingIdentifier, objectIdentifier)
|
||||
return err
|
||||
}
|
||||
|
||||
// ValidateAndSerialize implements AuditApi.ValidateAndSerialize
|
||||
func (a *MockAuditApi) ValidateAndSerialize(event *auditV1.AuditEvent, visibility auditV1.Visibility, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier) (SerializedPayload, error) {
|
||||
routableEvent, err := validateAndSerializePartially(a.validator, event, visibility, routingIdentifier, objectIdentifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return serializeToProtobufMessage(routableEvent)
|
||||
}
|
||||
|
||||
// Send implements AuditApi.Send
|
||||
func (a *MockAuditApi) Send(context.Context, *RoutingIdentifier, *SerializedPayload) error {
|
||||
return nil
|
||||
}
|
||||
41
api_mock_test.go
Normal file
41
api_mock_test.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
auditV1 "audit-schema/gen/go/audit/v1"
|
||||
"context"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMockAuditApi_Log(t *testing.T) {
|
||||
|
||||
auditApi, err := NewMockAuditApi()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate test data
|
||||
event, routingIdentifier, objectIdentifier := NewOrganizationAuditEvent(nil)
|
||||
|
||||
// Test
|
||||
t.Run("Log", func(t *testing.T) {
|
||||
assert.Nil(t, (*auditApi).Log(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, routingIdentifier, objectIdentifier))
|
||||
})
|
||||
|
||||
t.Run("ValidateAndSerialize", func(t *testing.T) {
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
serializedPayload, err := (*auditApi).ValidateAndSerialize(event, visibility, routingIdentifier, objectIdentifier)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateProtobufMessagePayload(t, serializedPayload.GetPayload(), routingIdentifier, objectIdentifier, event, event.EventName, visibility)
|
||||
})
|
||||
|
||||
t.Run("ValidateAndSerialize event nil", func(t *testing.T) {
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
_, err := (*auditApi).ValidateAndSerialize(nil, visibility, routingIdentifier, objectIdentifier)
|
||||
assert.ErrorIs(t, err, ErrEventNil)
|
||||
})
|
||||
|
||||
t.Run("Send", func(t *testing.T) {
|
||||
var payload SerializedPayload = &routablePayload{}
|
||||
assert.Nil(t, (*auditApi).Send(context.Background(), routingIdentifier, &payload))
|
||||
})
|
||||
}
|
||||
114
api_routable.go
Normal file
114
api_routable.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
auditV1 "audit-schema/gen/go/audit/v1"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// routablePayload implements SerializedPayload.
|
||||
type routablePayload struct {
|
||||
payload []byte
|
||||
contentType string
|
||||
}
|
||||
|
||||
// GetPayload implements SerializedPayload.GetPayload.
|
||||
func (p *routablePayload) GetPayload() []byte {
|
||||
return p.payload
|
||||
}
|
||||
|
||||
// GetContentType implements SerializedPayload.GetContentType.
|
||||
func (p *routablePayload) GetContentType() string {
|
||||
return p.contentType
|
||||
}
|
||||
|
||||
// routableTopicNameResolver implements TopicNameResolver.
|
||||
// Resolves topic names by concatenating topic type prefixes with routing identifiers.
|
||||
type routableTopicNameResolver struct {
|
||||
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(routingIdentifier *RoutingIdentifier) (string, error) {
|
||||
if routingIdentifier == nil {
|
||||
return r.systemTopicName, nil
|
||||
}
|
||||
switch routingIdentifier.Type {
|
||||
case RoutingIdentifierTypeOrganization:
|
||||
return fmt.Sprintf("topic://%s/%s", r.organizationTopicPrefix, routingIdentifier.Identifier), nil
|
||||
case RoutingIdentifierTypeProject:
|
||||
return fmt.Sprintf("topic://%s/%s", r.ProjectTopicPrefix, routingIdentifier.Identifier), nil
|
||||
default:
|
||||
return "", ErrUnsupportedObjectIdentifierType
|
||||
}
|
||||
}
|
||||
|
||||
// topicNameConfig provides topic name information required for the topic name resolution.
|
||||
type topicNameConfig struct {
|
||||
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 *MessagingApi
|
||||
topicNameResolver *TopicNameResolver
|
||||
validator *ProtobufValidator
|
||||
}
|
||||
|
||||
// NewRoutableAuditApi can be used to initialize the audit log api.
|
||||
func newRoutableAuditApi(messagingApi *MessagingApi, topicNameConfig topicNameConfig, validator ProtobufValidator) (*AuditApi, error) {
|
||||
|
||||
if messagingApi == nil {
|
||||
return nil, errors.New("messaging api nil")
|
||||
}
|
||||
|
||||
// Topic resolver
|
||||
var topicNameResolver TopicNameResolver = &routableTopicNameResolver{
|
||||
organizationTopicPrefix: topicNameConfig.OrganizationTopicPrefix,
|
||||
ProjectTopicPrefix: topicNameConfig.ProjectTopicPrefix,
|
||||
systemTopicName: topicNameConfig.SystemTopicName,
|
||||
}
|
||||
|
||||
// Audit api
|
||||
var auditApi AuditApi = &routableAuditApi{
|
||||
messagingApi: messagingApi,
|
||||
topicNameResolver: &topicNameResolver,
|
||||
validator: &validator,
|
||||
}
|
||||
|
||||
return &auditApi, nil
|
||||
}
|
||||
|
||||
// Log implements AuditApi.Log
|
||||
func (a *routableAuditApi) Log(ctx context.Context, event *auditV1.AuditEvent, visibility auditV1.Visibility, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier) error {
|
||||
|
||||
serializedPayload, err := a.ValidateAndSerialize(event, visibility, routingIdentifier, objectIdentifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.Send(ctx, routingIdentifier, &serializedPayload)
|
||||
}
|
||||
|
||||
// ValidateAndSerialize implements AuditApi.ValidateAndSerialize
|
||||
func (a *routableAuditApi) ValidateAndSerialize(event *auditV1.AuditEvent, visibility auditV1.Visibility, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier) (SerializedPayload, error) {
|
||||
routableEvent, err := validateAndSerializePartially(a.validator, event, visibility, routingIdentifier, objectIdentifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return serializeToProtobufMessage(routableEvent)
|
||||
}
|
||||
|
||||
// Send implements AuditApi.Send
|
||||
func (a *routableAuditApi) Send(ctx context.Context, routingIdentifier *RoutingIdentifier, serializedPayload *SerializedPayload) error {
|
||||
return send(a.topicNameResolver, a.messagingApi, ctx, routingIdentifier, serializedPayload)
|
||||
}
|
||||
396
api_routable_test.go
Normal file
396
api_routable_test.go
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
auditV1 "audit-schema/gen/go/audit/v1"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Azure/go-amqp"
|
||||
"github.com/bufbuild/protovalidate-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
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 := NewSolaceContainer(context.Background())
|
||||
assert.NoError(t, err)
|
||||
defer solaceContainer.Stop()
|
||||
|
||||
// Instantiate the messaging api
|
||||
messagingApi, err := NewAmqpMessagingApi(AmqpConfig{URL: solaceContainer.AmqpConnectionString})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Validator
|
||||
validator, err := protovalidate.New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Instantiate the audit api
|
||||
organizationTopicPrefix := "org"
|
||||
projectTopicPrefix := "project"
|
||||
systemTopicName := "topic://system/admin-events"
|
||||
auditApi, err := newRoutableAuditApi(
|
||||
messagingApi,
|
||||
topicNameConfig{OrganizationTopicPrefix: organizationTopicPrefix, ProjectTopicPrefix: projectTopicPrefix, SystemTopicName: systemTopicName},
|
||||
validator,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 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, routingIdentifier, objectIdentifier := NewOrganizationAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
assert.NoError(t, (*auditApi).Log(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
routingIdentifier,
|
||||
objectIdentifier,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentEvent(t, organizationTopicPrefix, message, routingIdentifier, objectIdentifier, event, "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, routingIdentifier, objectIdentifier := NewOrganizationAuditEvent(nil)
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, fmt.Sprintf("org/%s", routingIdentifier.Identifier)))
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
assert.NoError(t,
|
||||
(*auditApi).Log(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
routingIdentifier,
|
||||
objectIdentifier,
|
||||
))
|
||||
|
||||
// Receive the event from solace
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentEvent(t, organizationTopicPrefix, message, routingIdentifier, objectIdentifier, event, "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, "org/*"))
|
||||
|
||||
// Instantiate test data
|
||||
event, routingIdentifier, objectIdentifier := NewFolderAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
assert.NoError(t, (*auditApi).Log(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
routingIdentifier,
|
||||
objectIdentifier,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentEvent(t, organizationTopicPrefix, message, routingIdentifier, objectIdentifier, event, "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, "org/*"))
|
||||
|
||||
// Instantiate test data
|
||||
event, routingIdentifier, objectIdentifier := NewFolderAuditEvent(nil)
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, fmt.Sprintf("org/%s", routingIdentifier.Identifier)))
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
assert.NoError(t,
|
||||
(*auditApi).Log(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
routingIdentifier,
|
||||
objectIdentifier,
|
||||
))
|
||||
|
||||
// Receive the event from solace
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentEvent(t, organizationTopicPrefix, message, routingIdentifier, objectIdentifier, event, "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, routingIdentifier, objectIdentifier := NewProjectAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
assert.NoError(t,
|
||||
(*auditApi).Log(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
routingIdentifier,
|
||||
objectIdentifier,
|
||||
))
|
||||
|
||||
// Receive the event from solace
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentEvent(t, projectTopicPrefix, message, routingIdentifier, objectIdentifier, event, "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, routingIdentifier, objectIdentifier := NewProjectAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
assert.NoError(t,
|
||||
(*auditApi).Log(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
routingIdentifier,
|
||||
objectIdentifier,
|
||||
))
|
||||
|
||||
// Receive the event from solace
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentEvent(t, projectTopicPrefix, message, routingIdentifier, objectIdentifier, event, "PROJECT_CREATED", visibility)
|
||||
})
|
||||
|
||||
// Check logging of system events
|
||||
t.Run("Log private system event", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
queueName := "system-event-private"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "system/*"))
|
||||
|
||||
// Instantiate test data
|
||||
event := NewSystemAuditEvent(nil)
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PRIVATE
|
||||
assert.NoError(t,
|
||||
(*auditApi).Log(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
nil,
|
||||
nil,
|
||||
))
|
||||
|
||||
// Receive the event from solace
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check topic name
|
||||
assert.Equal(t, systemTopicName, *message.Properties.To)
|
||||
|
||||
// Check deserialized message
|
||||
var protobufMessage auditV1.ProtobufMessage
|
||||
assert.NoError(t, proto.Unmarshal(message.Data[0], &protobufMessage))
|
||||
assert.Equal(t, "audit.v1.RoutableAuditEvent", protobufMessage.ProtobufType)
|
||||
|
||||
validateProtobufMessagePayload(t, message.Data[0], nil, nil, event, "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, routingIdentifier, objectIdentifier := NewOrganizationAuditEventWithDetails()
|
||||
|
||||
// Log the event to solace
|
||||
visibility := auditV1.Visibility_VISIBILITY_PUBLIC
|
||||
assert.NoError(t, (*auditApi).Log(
|
||||
ctx,
|
||||
event,
|
||||
visibility,
|
||||
routingIdentifier,
|
||||
objectIdentifier,
|
||||
))
|
||||
|
||||
message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateSentEvent(t, organizationTopicPrefix, message, routingIdentifier, objectIdentifier, event, "ORGANIZATION_CREATED", visibility)
|
||||
})
|
||||
}
|
||||
|
||||
func validateSentEvent(t *testing.T, topicPrefix string, message *amqp.Message, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier, event *auditV1.AuditEvent, eventName string, visibility auditV1.Visibility) {
|
||||
|
||||
// Check topic name
|
||||
assert.Equal(t,
|
||||
fmt.Sprintf("topic://%s/%s", topicPrefix, routingIdentifier.Identifier),
|
||||
*message.Properties.To)
|
||||
|
||||
// Check deserialized message
|
||||
var protobufMessage auditV1.ProtobufMessage
|
||||
assert.NoError(t, proto.Unmarshal(message.Data[0], &protobufMessage))
|
||||
assert.Equal(t, "audit.v1.RoutableAuditEvent", protobufMessage.ProtobufType)
|
||||
|
||||
validateProtobufMessagePayload(t, message.Data[0], routingIdentifier, objectIdentifier, event, eventName, visibility)
|
||||
}
|
||||
|
||||
func validateProtobufMessagePayload(t *testing.T, payload []byte, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier, event *auditV1.AuditEvent, eventName string, visibility auditV1.Visibility) {
|
||||
|
||||
// Check deserialized message
|
||||
var protobufMessage auditV1.ProtobufMessage
|
||||
assert.NoError(t, proto.Unmarshal(payload, &protobufMessage))
|
||||
assert.Equal(t, "audit.v1.RoutableAuditEvent", protobufMessage.ProtobufType)
|
||||
|
||||
// Check routable audit event parameters
|
||||
var routableAuditEvent auditV1.RoutableAuditEvent
|
||||
assert.NoError(t, proto.Unmarshal(protobufMessage.Value, &routableAuditEvent))
|
||||
|
||||
assert.Equal(t, eventName, routableAuditEvent.EventName)
|
||||
assert.Equal(t, visibility, routableAuditEvent.Visibility)
|
||||
|
||||
switch reference := routableAuditEvent.ResourceReference.(type) {
|
||||
case *auditV1.RoutableAuditEvent_ObjectIdentifier:
|
||||
assert.True(t, proto.Equal(objectIdentifier, reference.ObjectIdentifier))
|
||||
|
||||
switch routingIdentifier.Type {
|
||||
case RoutingIdentifierTypeOrganization:
|
||||
if objectIdentifier.Type == auditV1.ObjectType_OBJECT_TYPE_ORGANIZATION {
|
||||
assert.Equal(t, routingIdentifier.Identifier.String(), reference.ObjectIdentifier.Identifier)
|
||||
}
|
||||
break
|
||||
case RoutingIdentifierTypeProject:
|
||||
assert.Equal(t, routingIdentifier.Identifier.String(), reference.ObjectIdentifier.Identifier)
|
||||
assert.Equal(t, auditV1.ObjectType_OBJECT_TYPE_PROJECT, reference.ObjectIdentifier.Type)
|
||||
break
|
||||
default:
|
||||
assert.Fail(t, "Routing identifier type not expected")
|
||||
}
|
||||
break
|
||||
case *auditV1.RoutableAuditEvent_ObjectName:
|
||||
assert.Nil(t, objectIdentifier)
|
||||
assert.Nil(t, routingIdentifier)
|
||||
assert.Equal(t, auditV1.ObjectName_OBJECT_NAME_SYSTEM, reference.ObjectName)
|
||||
default:
|
||||
assert.Fail(t, "Object name not expected")
|
||||
}
|
||||
|
||||
var auditEvent auditV1.AuditEvent
|
||||
switch data := routableAuditEvent.Data.(type) {
|
||||
case *auditV1.RoutableAuditEvent_UnencryptedData:
|
||||
assert.NoError(t, proto.Unmarshal(data.UnencryptedData.Data, &auditEvent))
|
||||
break
|
||||
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(&RoutingIdentifier{Type: 2})
|
||||
assert.ErrorIs(t, err, ErrUnsupportedObjectIdentifierType)
|
||||
}
|
||||
|
||||
func TestNewRoutableAuditApi_NewRoutableAuditApi_MessagingApiNil(t *testing.T) {
|
||||
auditApi, err := newRoutableAuditApi(nil, topicNameConfig{}, nil)
|
||||
assert.Nil(t, auditApi)
|
||||
assert.EqualError(t, err, "messaging api nil")
|
||||
}
|
||||
|
||||
func TestRoutableAuditApi_ValidateAndSerialize_ValidationFailed(t *testing.T) {
|
||||
expectedError := errors.New("expected error")
|
||||
|
||||
validator := &ProtobufValidatorMock{}
|
||||
validator.On("Validate", mock.Anything).Return(expectedError)
|
||||
var protobufValidator ProtobufValidator = validator
|
||||
|
||||
auditApi := routableAuditApi{validator: &protobufValidator}
|
||||
|
||||
event := NewSystemAuditEvent(nil)
|
||||
_, err := auditApi.ValidateAndSerialize(event, auditV1.Visibility_VISIBILITY_PUBLIC, nil, nil)
|
||||
assert.ErrorIs(t, err, expectedError)
|
||||
}
|
||||
|
||||
func TestRoutableAuditApi_Log_ValidationFailed(t *testing.T) {
|
||||
expectedError := errors.New("expected error")
|
||||
|
||||
validator := &ProtobufValidatorMock{}
|
||||
validator.On("Validate", mock.Anything).Return(expectedError)
|
||||
var protobufValidator ProtobufValidator = validator
|
||||
|
||||
auditApi := routableAuditApi{validator: &protobufValidator}
|
||||
|
||||
event := NewSystemAuditEvent(nil)
|
||||
err := auditApi.Log(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, nil, nil)
|
||||
assert.ErrorIs(t, err, expectedError)
|
||||
}
|
||||
|
||||
func TestRoutableAuditApi_Log_NilEvent(t *testing.T) {
|
||||
auditApi := routableAuditApi{}
|
||||
err := auditApi.Log(context.Background(), nil, auditV1.Visibility_VISIBILITY_PUBLIC, nil, nil)
|
||||
assert.ErrorIs(t, err, ErrEventNil)
|
||||
}
|
||||
15
audit-schema.iml
Normal file
15
audit-schema.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
|
||||
219
client.go
Normal file
219
client.go
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
package main
|
||||
|
||||
// terraform-provider-solacebroker
|
||||
//
|
||||
// Copyright 2024 Solace Corporation. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrResourceNotFound = errors.New("resource not found")
|
||||
)
|
||||
|
||||
var firstRequest = true
|
||||
|
||||
type Client struct {
|
||||
*retryablehttp.Client
|
||||
url string
|
||||
username string
|
||||
password string
|
||||
bearerToken string
|
||||
retries uint
|
||||
retryMinInterval time.Duration
|
||||
retryMaxInterval time.Duration
|
||||
requestMinInterval time.Duration
|
||||
requestTimeout time.Duration
|
||||
rateLimiter <-chan time.Time
|
||||
}
|
||||
|
||||
type Option func(*Client)
|
||||
|
||||
func BasicAuth(username, password string) Option {
|
||||
return func(client *Client) {
|
||||
client.username = username
|
||||
client.password = password
|
||||
}
|
||||
}
|
||||
|
||||
//func BearerToken(bearerToken string) Option {
|
||||
// return func(client *Client) {
|
||||
// client.bearerToken = bearerToken
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//func Retries(numRetries uint, retryMinInterval, retryMaxInterval time.Duration) Option {
|
||||
// return func(client *Client) {
|
||||
// client.retries = numRetries
|
||||
// client.retryMinInterval = retryMinInterval
|
||||
// client.retryMaxInterval = retryMaxInterval
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//func RequestLimits(requestTimeoutDuration, requestMinInterval time.Duration) Option {
|
||||
// return func(client *Client) {
|
||||
// client.requestTimeout = requestTimeoutDuration
|
||||
// client.requestMinInterval = requestMinInterval
|
||||
// }
|
||||
//}
|
||||
|
||||
func NewClient(url string, insecure_skip_verify bool, providerClient bool, options ...Option) *Client {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure_skip_verify},
|
||||
MaxIdleConnsPerHost: 10,
|
||||
}
|
||||
retryClient := retryablehttp.NewClient()
|
||||
retryClient.HTTPClient.Transport = tr
|
||||
if !providerClient {
|
||||
retryClient.Logger = nil
|
||||
}
|
||||
client := &Client{
|
||||
Client: retryClient,
|
||||
url: url,
|
||||
retries: 10, // default 3
|
||||
retryMinInterval: time.Second,
|
||||
retryMaxInterval: time.Second * 10,
|
||||
}
|
||||
for _, o := range options {
|
||||
o(client)
|
||||
}
|
||||
client.Client.RetryMax = int(client.retries)
|
||||
client.Client.RetryWaitMin = client.retryMinInterval
|
||||
client.Client.RetryWaitMax = client.retryMaxInterval
|
||||
client.HTTPClient.Timeout = client.requestTimeout
|
||||
client.HTTPClient.Jar, _ = cookiejar.New(nil)
|
||||
if client.requestMinInterval > 0 {
|
||||
client.rateLimiter = time.NewTicker(client.requestMinInterval).C
|
||||
} else {
|
||||
ch := make(chan time.Time)
|
||||
// closing the channel will make receiving from the channel non-blocking (the value received will be the
|
||||
// zero value)
|
||||
close(ch)
|
||||
client.rateLimiter = ch
|
||||
}
|
||||
firstRequest = true
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *Client) RequestWithBody(ctx context.Context, method, url string, body any) (map[string]any, error) {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, method, c.url+url, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
slog.Debug(fmt.Sprintf("===== %v to %v =====", request.Method, request.URL))
|
||||
rawBody, err := c.doRequest(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseResponseAsObject(ctx, request, rawBody)
|
||||
}
|
||||
|
||||
func (c *Client) doRequest(request *http.Request) ([]byte, error) {
|
||||
if !firstRequest {
|
||||
// the value doesn't matter, it is waiting for the value that matters
|
||||
<-c.rateLimiter
|
||||
} else {
|
||||
// only skip rate limiter for the first request
|
||||
firstRequest = false
|
||||
}
|
||||
if request.Method != http.MethodGet {
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
// Prefer OAuth even if Basic Auth credentials provided
|
||||
if c.bearerToken != "" {
|
||||
request.Header.Set("Authorization", "Bearer "+c.bearerToken)
|
||||
} else if c.username != "" {
|
||||
request.SetBasicAuth(c.username, c.password)
|
||||
} else {
|
||||
return nil, fmt.Errorf("either username or bearer token must be provided to access the broker")
|
||||
}
|
||||
var response *http.Response
|
||||
var err error
|
||||
response, err = c.StandardClient().Do(request)
|
||||
if err != nil || response == 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("could not perform request: status %v (%v) during %v to %v, response body:\n%s", response.StatusCode, response.Status, request.Method, request.URL, rawBody)
|
||||
}
|
||||
if _, err := io.Copy(io.Discard, response.Body); err != nil {
|
||||
return nil, fmt.Errorf("response processing error: during %v to %v", request.Method, request.URL)
|
||||
}
|
||||
return rawBody, nil
|
||||
}
|
||||
|
||||
func parseResponseAsObject(ctx context.Context, request *http.Request, 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 body from %v to %v, response body was:\n%s", request.Method, request.URL, dataResponse)
|
||||
}
|
||||
rawData, ok := data["data"]
|
||||
if ok {
|
||||
// Valid data
|
||||
data, _ = rawData.(map[string]any)
|
||||
return data, nil
|
||||
} else {
|
||||
// Analize response metadata details
|
||||
rawData, ok = data["meta"]
|
||||
if ok {
|
||||
data, _ = rawData.(map[string]any)
|
||||
if data["responseCode"].(float64) == http.StatusOK {
|
||||
// this is valid response for 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 is a special type we want to return
|
||||
return nil, fmt.Errorf("request failed from %v to %v, %v, %v, %w", request.Method, request.URL, description, status, ErrResourceNotFound)
|
||||
}
|
||||
slog.Error(fmt.Sprintf("SEMP request returned %v, %v", description, status))
|
||||
return nil, fmt.Errorf("request failed for %v using %v, %v, %v", request.URL, request.Method, description, status)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("could not parse response details from %v to %v, response body was:\n%s", request.Method, request.URL, dataResponse)
|
||||
}
|
||||
|
||||
func (c *Client) RequestWithoutBody(ctx context.Context, method, url string) (map[string]interface{}, error) {
|
||||
request, err := http.NewRequestWithContext(ctx, method, c.url+url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
slog.Debug(fmt.Sprintf("===== %v to %v =====", request.Method, request.URL))
|
||||
rawBody, err := c.doRequest(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseResponseAsObject(ctx, request, rawBody)
|
||||
}
|
||||
74
go.mod
Normal file
74
go.mod
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
module audit-schema
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240401165935-b983156c5e99.1
|
||||
github.com/Azure/go-amqp v1.0.5
|
||||
github.com/bufbuild/protovalidate-go v0.6.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/testcontainers/testcontainers-go v0.31.0
|
||||
google.golang.org/protobuf v1.33.0
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.4 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/containerd/containerd v1.7.15 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/cpuguy83/dockercfg v0.3.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.5.0 // indirect
|
||||
github.com/docker/docker v25.0.5+incompatible // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/cel-go v0.20.1 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/klauspost/compress v1.16.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/sys/sequential v0.5.0 // indirect
|
||||
github.com/moby/sys/user v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
golang.org/x/crypto v0.22.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.19.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect
|
||||
google.golang.org/grpc v1.62.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
231
go.sum
Normal file
231
go.sum
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240401165935-b983156c5e99.1 h1:2IGhRovxlsOIQgx2ekZWo4wTPAYpck41+18ICxs37is=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240401165935-b983156c5e99.1/go.mod h1:Tgn5bgL220vkFOI0KPStlcClPeOJzAv4uT+V8JXGUnw=
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-amqp v1.0.5 h1:po5+ljlcNSU8xtapHTe8gIc8yHxCzC03E8afH2g1ftU=
|
||||
github.com/Azure/go-amqp v1.0.5/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=
|
||||
github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||
github.com/bufbuild/protovalidate-go v0.6.2 h1:U/V3CGF0kPlR12v41rjO4DrYZtLcS4ZONLmWN+rJVCQ=
|
||||
github.com/bufbuild/protovalidate-go v0.6.2/go.mod h1:4BR3rKEJiUiTy+sqsusFn2ladOf0kYmA2Reo6BHSBgQ=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/containerd/containerd v1.7.15 h1:afEHXdil9iAm03BmhjzKyXnnEBtjaLJefdU7DV0IFes=
|
||||
github.com/containerd/containerd v1.7.15/go.mod h1:ISzRRTMF8EXNpJlTzyr2XMhN+j9K302C21/+cr3kUnY=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E=
|
||||
github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
|
||||
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE=
|
||||
github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
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.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84=
|
||||
github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
|
||||
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
|
||||
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
|
||||
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
|
||||
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
|
||||
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
||||
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
|
||||
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
|
||||
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/testcontainers/testcontainers-go v0.31.0 h1:W0VwIhcEVhRflwL9as3dhY6jXjVCA27AkmbnZ+UTh3U=
|
||||
github.com/testcontainers/testcontainers-go v0.31.0/go.mod h1:D2lAoA0zUFiSY+eAflqK5mcUx/A5hrrORaEQrd0SefI=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
||||
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
||||
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
|
||||
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
|
||||
go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
|
||||
go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
|
||||
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
|
||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda h1:b6F6WIV4xHHD0FA4oIyzU6mHWg2WI2X1RBehwa5QN38=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda/go.mod h1:AHcE/gZH76Bk/ROZhQphlRoWo5xKDEtz3eVEO1LfA8c=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
|
||||
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
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.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
|
||||
gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
115
main.go
Normal file
115
main.go
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
auditV1 "audit-schema/gen/go/audit/v1"
|
||||
"fmt"
|
||||
"github.com/bufbuild/protovalidate-go"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
"google.golang.org/protobuf/reflect/protoregistry"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
func main() {
|
||||
agent := "aaa"
|
||||
parameters, _ := structpb.NewValue(map[string]any{
|
||||
"parameter": "b",
|
||||
})
|
||||
body, _ := structpb.NewValue(map[string]any{
|
||||
"body": "b",
|
||||
})
|
||||
auditEvent := auditV1.AuditEvent{
|
||||
EventName: "XXX",
|
||||
EventTrigger: auditV1.EventTrigger_EVENT_TRIGGER_REQUEST,
|
||||
Request: &auditV1.RequestDetails{
|
||||
Endpoint: "XXX",
|
||||
SourceIpAddress: "127.0.0.1",
|
||||
UserAgent: &agent,
|
||||
Parameters: parameters.GetStructValue(),
|
||||
Body: body.GetStructValue(),
|
||||
Headers: []*auditV1.RequestHeader{
|
||||
{
|
||||
Key: "abc",
|
||||
Value: "def",
|
||||
},
|
||||
},
|
||||
},
|
||||
EventTimeStamp: nil,
|
||||
Initiator: nil,
|
||||
Principals: nil,
|
||||
ResourceId: nil,
|
||||
ResourceName: nil,
|
||||
CorrelationId: nil,
|
||||
Details: nil,
|
||||
}
|
||||
|
||||
auditEventBytes, err := proto.Marshal(&auditEvent)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
payload := auditV1.UnencryptedData{
|
||||
Data: auditEventBytes,
|
||||
ProtobufType: fmt.Sprintf("%v", auditEvent.ProtoReflect().Descriptor().FullName()),
|
||||
}
|
||||
|
||||
//var version int32 = 0
|
||||
routableEvent := auditV1.RoutableAuditEvent{
|
||||
EventName: "A_V1",
|
||||
Visibility: auditV1.Visibility_VISIBILITY_PRIVATE,
|
||||
ResourceReference: &auditV1.RoutableAuditEvent_ObjectName{ObjectName: auditV1.ObjectName_OBJECT_NAME_SYSTEM},
|
||||
Data: &auditV1.RoutableAuditEvent_UnencryptedData{UnencryptedData: &payload},
|
||||
}
|
||||
|
||||
validator, err := protovalidate.New()
|
||||
if err != nil {
|
||||
fmt.Println("failed to initialize validator:", err)
|
||||
}
|
||||
err = validator.Validate(&auditEvent)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
|
||||
err = validator.Validate(&routableEvent)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
|
||||
routableEventBytes, err := proto.Marshal(&routableEvent)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var deserializedRoutableEvent auditV1.RoutableAuditEvent
|
||||
err = proto.Unmarshal(routableEventBytes, &deserializedRoutableEvent)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
if proto.Equal(&routableEvent, &deserializedRoutableEvent) {
|
||||
fmt.Println("Event Matched")
|
||||
} else {
|
||||
fmt.Println("Event Not Matched")
|
||||
}
|
||||
//fmt.Println(deserializedRoutableEvent.String())
|
||||
|
||||
protoMessage := getProto(deserializedRoutableEvent.GetUnencryptedData().ProtobufType)
|
||||
err = proto.Unmarshal(deserializedRoutableEvent.GetUnencryptedData().Data, protoMessage)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
deserializedAuditEvent, isAuditEvent := protoMessage.(*auditV1.AuditEvent)
|
||||
if isAuditEvent {
|
||||
fmt.Println(deserializedAuditEvent.String())
|
||||
}
|
||||
|
||||
fmt.Println(string(auditEventBytes))
|
||||
}
|
||||
|
||||
func getProto(dataType string) protoreflect.ProtoMessage {
|
||||
t, _ := protoregistry.GlobalTypes.FindMessageByName(protoreflect.FullName(dataType))
|
||||
m := t.New().Interface()
|
||||
return m
|
||||
}
|
||||
37
main.py
Normal file
37
main.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Install protovalidate:
|
||||
# python3 -m venv path/to/venv
|
||||
# source path/to/venv/bin/activate
|
||||
# pip3 install -r requirements.txt
|
||||
import protovalidate
|
||||
|
||||
from audit.v1.audit_event_pb2 import AuditEvent, EventTrigger, RequestDetails
|
||||
from audit.v1.routable_event_pb2 import RoutableAuditEvent, UnencryptedData
|
||||
|
||||
audit_event = AuditEvent()
|
||||
audit_event.event_name = "XXX"
|
||||
audit_event.event_trigger = EventTrigger.EVENT_REQUEST
|
||||
|
||||
audit_event.request.endpoint = "XXX"
|
||||
audit_event.request.source_ip_address = "127.0.0.1"
|
||||
audit_event.request.user_agent = "aaa"
|
||||
|
||||
routable_audit_event = RoutableAuditEvent()
|
||||
routable_audit_event.event_name = "AAA"
|
||||
|
||||
routable_audit_event.unencrypted_data.data = audit_event.SerializeToString()
|
||||
routable_audit_event.unencrypted_data.protobuf_type = audit_event.DESCRIPTOR.full_name
|
||||
|
||||
try:
|
||||
protovalidate.validate(audit_event)
|
||||
except protovalidate.ValidationError as e:
|
||||
print("AuditEvent validation errors:")
|
||||
for error in e.errors():
|
||||
print("field_path: " + error.field_path + ", constraint_id: " + error.constraint_id + ", message: " + error.message)
|
||||
try:
|
||||
protovalidate.validate(routable_audit_event)
|
||||
except protovalidate.ValidationError as e:
|
||||
print("\nRoutableAuditEvent validation errors:")
|
||||
for error in e.errors():
|
||||
print("field_path: " + error.field_path + ", constraint_id: " + error.constraint_id + ", message: " + error.message)
|
||||
|
||||
print(audit_event.SerializeToString())
|
||||
196
messaging.go
Normal file
196
messaging.go
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/Azure/go-amqp"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Default connection timeout for the AMQP connection
|
||||
const connectionTimeoutSeconds = 10
|
||||
|
||||
// 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 routing identifier
|
||||
Resolve(routingIdentifier *RoutingIdentifier) (string, error)
|
||||
}
|
||||
|
||||
// MessagingApi is an abstraction for a messaging system that can be used to send
|
||||
// audit logs to the audit log system.
|
||||
type MessagingApi 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
|
||||
//
|
||||
// It returns technical errors for connection issues or sending problems.
|
||||
Send(ctx context.Context, topic string, data []byte, contentType string) error
|
||||
}
|
||||
|
||||
// AmqpConfig provides AMQP connection related parameters.
|
||||
type AmqpConfig struct {
|
||||
URL string
|
||||
User string
|
||||
Password string
|
||||
}
|
||||
|
||||
// AmqpSession is an abstraction providing a subset of the methods of amqp.Session
|
||||
type AmqpSession interface {
|
||||
NewSender(ctx context.Context, target string, opts *amqp.SenderOptions) (*AmqpSender, error)
|
||||
Close(ctx context.Context) error
|
||||
}
|
||||
|
||||
type AmqpSessionWrapper struct {
|
||||
session *amqp.Session
|
||||
}
|
||||
|
||||
func (w AmqpSessionWrapper) NewSender(ctx context.Context, target string, opts *amqp.SenderOptions) (*AmqpSender, error) {
|
||||
sender, err := w.session.NewSender(ctx, target, opts)
|
||||
var amqpSender AmqpSender = sender
|
||||
return &amqpSender, err
|
||||
}
|
||||
|
||||
func (w AmqpSessionWrapper) Close(ctx context.Context) error {
|
||||
return w.session.Close(ctx)
|
||||
}
|
||||
|
||||
// AmqpSender is an abstraction providing a subset of the methods of amqp.Sender
|
||||
type AmqpSender interface {
|
||||
Send(ctx context.Context, msg *amqp.Message, opts *amqp.SendOptions) error
|
||||
Close(ctx context.Context) error
|
||||
}
|
||||
|
||||
// AmqpMessagingApi implements MessagingApi.
|
||||
type AmqpMessagingApi struct {
|
||||
config AmqpConfig
|
||||
connection *amqp.Conn
|
||||
session *AmqpSession
|
||||
}
|
||||
|
||||
func NewAmqpMessagingApi(amqpConfig AmqpConfig) (*MessagingApi, error) {
|
||||
amqpApi := &AmqpMessagingApi{config: amqpConfig}
|
||||
|
||||
err := amqpApi.connect()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var messagingApi MessagingApi = amqpApi
|
||||
return &messagingApi, nil
|
||||
}
|
||||
|
||||
// connect opens a new connection and session to the AMQP messaging system.
|
||||
// The connection attempt will be cancelled after connectionTimeoutSeconds.
|
||||
func (a *AmqpMessagingApi) connect() error {
|
||||
slog.Info("connecting to messaging system")
|
||||
|
||||
// Set credentials if specified
|
||||
auth := amqp.SASLTypeAnonymous()
|
||||
|
||||
if a.config.User != "" && a.config.Password != "" {
|
||||
auth = amqp.SASLTypePlain(a.config.User, a.config.Password)
|
||||
slog.Info("using username and password for messaging")
|
||||
} else {
|
||||
slog.Warn("using anonymous messaging!")
|
||||
}
|
||||
|
||||
options := &amqp.ConnOptions{
|
||||
SASLType: auth,
|
||||
}
|
||||
|
||||
// Create new context with timeout for the connection initialization
|
||||
subCtx, cancel := context.WithTimeout(context.Background(), connectionTimeoutSeconds*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Initialize connection
|
||||
conn, err := amqp.Dial(subCtx, a.config.URL, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.connection = conn
|
||||
|
||||
// Initialize session
|
||||
session, err := conn.NewSession(context.Background(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var amqpSession AmqpSession = AmqpSessionWrapper{session: session}
|
||||
a.session = &amqpSession
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send implements MessagingApi.Send.
|
||||
// If errors occur the connection to the messaging system will be closed and re-established.
|
||||
func (a *AmqpMessagingApi) Send(ctx context.Context, topic string, data []byte, contentType string) error {
|
||||
err := a.trySend(ctx, topic, data, contentType)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Drop the current sender, as it cannot connect to the broker anymore
|
||||
slog.Error("message sender error, recreating", err)
|
||||
|
||||
err = a.resetConnection(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.trySend(ctx, topic, data, contentType)
|
||||
}
|
||||
|
||||
// trySend actually sends the given data as amqp.Message to the messaging system.
|
||||
func (a *AmqpMessagingApi) trySend(ctx context.Context, topic string, data []byte, contentType string) error {
|
||||
if !strings.HasPrefix(topic, AmqpTopicPrefix) {
|
||||
return fmt.Errorf(
|
||||
"topic %q name lacks mandatory prefix %q",
|
||||
topic,
|
||||
AmqpTopicPrefix,
|
||||
)
|
||||
}
|
||||
|
||||
sender, err := (*a.session).NewSender(ctx, topic, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bytes := [][]byte{data}
|
||||
message := amqp.Message{
|
||||
Header: &amqp.MessageHeader{
|
||||
Durable: true,
|
||||
},
|
||||
Properties: &amqp.MessageProperties{
|
||||
To: &topic,
|
||||
ContentType: &contentType,
|
||||
},
|
||||
Data: bytes,
|
||||
}
|
||||
|
||||
err = (*sender).Send(ctx, &message, nil)
|
||||
if err != nil {
|
||||
_ = (*sender).Close(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resetConnection closes the current session and connection and reconnects to the messaging system.
|
||||
func (a *AmqpMessagingApi) resetConnection(ctx context.Context) error {
|
||||
_ = (*a.session).Close(ctx)
|
||||
err := a.connection.Close()
|
||||
if err != nil {
|
||||
slog.Error("failed to close message connection", err)
|
||||
}
|
||||
|
||||
return a.connect()
|
||||
}
|
||||
172
messaging_test.go
Normal file
172
messaging_test.go
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Azure/go-amqp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MessagingApiMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MessagingApiMock) Send(ctx context.Context, topic string, data []byte, contentType string) error {
|
||||
args := m.Called(ctx, topic, data, contentType)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
type AmqpSessionMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *AmqpSessionMock) NewSender(ctx context.Context, target string, opts *amqp.SenderOptions) (*AmqpSender, error) {
|
||||
args := m.Called(ctx, target, opts)
|
||||
var sender *AmqpSender = nil
|
||||
if args.Get(0) != nil {
|
||||
sender = args.Get(0).(*AmqpSender)
|
||||
}
|
||||
err := args.Error(1)
|
||||
return sender, err
|
||||
}
|
||||
|
||||
func (m *AmqpSessionMock) Close(ctx context.Context) error {
|
||||
args := m.Called(ctx)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
type AmqpSenderMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *AmqpSenderMock) Send(ctx context.Context, msg *amqp.Message, opts *amqp.SendOptions) error {
|
||||
args := m.Called(ctx, msg, opts)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *AmqpSenderMock) Close(ctx context.Context) error {
|
||||
args := m.Called(ctx)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func Test_NewAmqpMessagingApi(t *testing.T) {
|
||||
_, err := NewAmqpMessagingApi(AmqpConfig{URL: "not-handled-protocol://localhost:5672"})
|
||||
assert.EqualError(t, err, "unsupported scheme \"not-handled-protocol\"")
|
||||
}
|
||||
|
||||
func TestAmqpMessagingApi_Send(t *testing.T) {
|
||||
// Specify test timeout
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancelFn()
|
||||
|
||||
// Start solace docker container
|
||||
solaceContainer, err := NewSolaceContainer(context.Background())
|
||||
assert.NoError(t, err)
|
||||
defer solaceContainer.Stop()
|
||||
|
||||
t.Run("Missing topic prefix", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
api, err := NewAmqpMessagingApi(AmqpConfig{URL: solaceContainer.AmqpConnectionString})
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = (*api).Send(ctx, "topic-name", []byte{}, "application/json")
|
||||
assert.EqualError(t, err, "topic \"topic-name\" name lacks mandatory prefix \"topic://\"")
|
||||
})
|
||||
|
||||
t.Run("New sender call returns error", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Initialize the solace queue
|
||||
topicSubscriptionTopicPattern := "auditlog/>"
|
||||
queueName := "messaging-new-sender"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
topicName := fmt.Sprintf("topic://auditlog/%s", "amqp-no-new-sender")
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
api := &AmqpMessagingApi{config: AmqpConfig{URL: solaceContainer.AmqpConnectionString}}
|
||||
err := api.connect()
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedError := errors.New("expected error")
|
||||
|
||||
// Set mock session
|
||||
sessionMock := AmqpSessionMock{}
|
||||
sessionMock.On("NewSender", mock.Anything, mock.Anything, mock.Anything).Return(nil, expectedError)
|
||||
sessionMock.On("Close", mock.Anything).Return(nil)
|
||||
|
||||
var amqpSession AmqpSession = &sessionMock
|
||||
api.session = &amqpSession
|
||||
|
||||
// It's expected that the test succeeds.
|
||||
// First the session is closed as it returns the expected error
|
||||
// Then the retry mechanism restarts the connection and successfully sends the data
|
||||
value := "test"
|
||||
err = (*api).Send(ctx, topicName, []byte(value), "application/json")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check that the mock was called
|
||||
assert.True(t, sessionMock.AssertNumberOfCalls(t, "NewSender", 1))
|
||||
assert.True(t, sessionMock.AssertNumberOfCalls(t, "Close", 1))
|
||||
|
||||
message, err := solaceContainer.NextMessage(ctx, fmt.Sprintf("queue://%s", queueName), true)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, value, string(message.Data[0]))
|
||||
assert.Equal(t, topicName, *message.Properties.To)
|
||||
})
|
||||
|
||||
t.Run("Send call on sender returns error", func(t *testing.T) {
|
||||
defer solaceContainer.StopOnError()
|
||||
|
||||
// Initialize the solace queue
|
||||
topicSubscriptionTopicPattern := "auditlog/>"
|
||||
queueName := "messaging-sender-error"
|
||||
assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName))
|
||||
assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicSubscriptionTopicPattern))
|
||||
topicName := fmt.Sprintf("topic://auditlog/%s", "amqp-sender-error")
|
||||
assert.NoError(t, solaceContainer.ValidateTopicName(topicSubscriptionTopicPattern, topicName))
|
||||
|
||||
api := &AmqpMessagingApi{config: AmqpConfig{URL: solaceContainer.AmqpConnectionString}}
|
||||
err := api.connect()
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedError := errors.New("expected error")
|
||||
|
||||
// Instantiate mock sender
|
||||
senderMock := AmqpSenderMock{}
|
||||
senderMock.On("Send", mock.Anything, mock.Anything, mock.Anything).Return(expectedError)
|
||||
senderMock.On("Close", mock.Anything).Return(nil)
|
||||
var amqpSender AmqpSender = &senderMock
|
||||
|
||||
// Set mock session
|
||||
sessionMock := AmqpSessionMock{}
|
||||
sessionMock.On("NewSender", mock.Anything, mock.Anything, mock.Anything).Return(&amqpSender, nil)
|
||||
sessionMock.On("Close", mock.Anything).Return(nil)
|
||||
|
||||
var amqpSession AmqpSession = &sessionMock
|
||||
api.session = &amqpSession
|
||||
|
||||
// It's expected that the test succeeds.
|
||||
// First the sender and session are closed as the sender returns the expected error
|
||||
// Then the retry mechanism restarts the connection and successfully sends the data
|
||||
value := "test"
|
||||
err = (*api).Send(ctx, topicName, []byte(value), "application/json")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check that the mocks were called
|
||||
assert.True(t, sessionMock.AssertNumberOfCalls(t, "NewSender", 1))
|
||||
assert.True(t, sessionMock.AssertNumberOfCalls(t, "Close", 1))
|
||||
assert.True(t, senderMock.AssertNumberOfCalls(t, "Send", 1))
|
||||
assert.True(t, senderMock.AssertNumberOfCalls(t, "Close", 1))
|
||||
|
||||
message, err := solaceContainer.NextMessage(ctx, fmt.Sprintf("queue://%s", queueName), true)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, value, string(message.Data[0]))
|
||||
assert.Equal(t, topicName, *message.Properties.To)
|
||||
})
|
||||
}
|
||||
83
proto/audit/v1/audit_event.proto
Normal file
83
proto/audit/v1/audit_event.proto
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
syntax = "proto3";
|
||||
|
||||
import "buf/validate/validate.proto";
|
||||
import "google/protobuf/struct.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
package audit.v1;
|
||||
|
||||
option go_package = "./audit;auditV1";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "com.schwarz.stackit.audit.v1";
|
||||
|
||||
enum EventTrigger {
|
||||
EVENT_TRIGGER_UNSPECIFIED = 0;
|
||||
// Event from messaging system
|
||||
EVENT_TRIGGER_EVENT = 1;
|
||||
// Time based scheduler
|
||||
EVENT_TRIGGER_SCHEDULER = 2;
|
||||
// Network request (REST, gRPC, etc.)
|
||||
EVENT_TRIGGER_REQUEST = 3;
|
||||
}
|
||||
|
||||
message Principal {
|
||||
// A UUID or another kind of identifier
|
||||
string id = 1 [(buf.validate.field).required = true];
|
||||
optional string email = 2 [(buf.validate.field).string.email = true, (buf.validate.field).string.max_len = 255];
|
||||
}
|
||||
|
||||
message RequestDetails {
|
||||
string endpoint = 1 [(buf.validate.field).required = true, (buf.validate.field).string.min_len = 1, (buf.validate.field).string.max_len = 255];
|
||||
// Accepts ipv4 and ipv6
|
||||
string source_ip_address = 2 [(buf.validate.field).required = true, (buf.validate.field).string.ip = true];
|
||||
optional string user_agent = 3 [(buf.validate.field).required = true, (buf.validate.field).string.min_len = 1, (buf.validate.field).string.max_len = 255];
|
||||
optional google.protobuf.Struct parameters = 4;
|
||||
optional google.protobuf.Struct body = 5;
|
||||
repeated RequestHeader headers = 6;
|
||||
}
|
||||
|
||||
// Key-value pair for request headers. Key and value are mandatory.
|
||||
message RequestHeader {
|
||||
string key = 1 [(buf.validate.field).required = true, (buf.validate.field).string.min_len = 1];
|
||||
string value = 2 [(buf.validate.field).required = true, (buf.validate.field).string.min_len = 1];
|
||||
}
|
||||
|
||||
message AuditEvent {
|
||||
// Validate that "request" details are set if the event trigger is set to "EVENT_REQUEST"
|
||||
option (buf.validate.message).cel = {
|
||||
id: "request.details"
|
||||
message: "request details must be set"
|
||||
expression: "this.event_trigger == 3 && has(this.request) || this.event_trigger != 3"
|
||||
};
|
||||
|
||||
// Functional event name with pattern <TYPE>_<ACTION>, e.g. ORGANIZATION_CREATED
|
||||
// Important for filtering and translation / verbalization of event types
|
||||
// in the UI or data sinks.
|
||||
string event_name = 1 [(buf.validate.field).required = true, (buf.validate.field).string.pattern = "^[A-Z]+_[A-Z]+$"];
|
||||
|
||||
// The time when the event happened. Must not be a value in the future.
|
||||
google.protobuf.Timestamp event_time_stamp = 2 [(buf.validate.field).required = true, (buf.validate.field).timestamp.lt_now = true];
|
||||
|
||||
EventTrigger event_trigger = 3 [(buf.validate.field).required = true, (buf.validate.field).enum.defined_only = true];
|
||||
|
||||
// Request details - mandatory if event_trigger is set to "EVENT_REQUEST"
|
||||
optional RequestDetails request = 4;
|
||||
|
||||
Principal initiator = 5 [(buf.validate.field).required = true];
|
||||
|
||||
// List of service account delegation principals.
|
||||
// -> Chain from service account to the actual user who initiated the action.
|
||||
repeated Principal principals = 6;
|
||||
|
||||
optional string resource_id = 7 [(buf.validate.field).string.min_len = 1, (buf.validate.field).string.max_len = 255];
|
||||
|
||||
optional string resource_name = 8 [(buf.validate.field).string.min_len = 1, (buf.validate.field).string.max_len = 255];
|
||||
|
||||
optional string correlation_id = 9 [(buf.validate.field).string.min_len = 1, (buf.validate.field).string.max_len = 255];
|
||||
|
||||
// Result of the operation to publish with the event
|
||||
optional google.protobuf.Struct result = 10;
|
||||
|
||||
// Additional information to publish with the event
|
||||
optional google.protobuf.Struct details = 11;
|
||||
}
|
||||
17
proto/audit/v1/event.proto
Normal file
17
proto/audit/v1/event.proto
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
syntax = "proto3";
|
||||
|
||||
import "buf/validate/validate.proto";
|
||||
|
||||
package audit.v1;
|
||||
|
||||
option go_package = "./audit;auditV1";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "com.schwarz.stackit.audit.v1";
|
||||
|
||||
message ProtobufMessage {
|
||||
// Serialized event
|
||||
bytes value = 1 [(buf.validate.field).required = true, (buf.validate.field).bytes.min_len = 1];
|
||||
|
||||
// Name of the protobuf type
|
||||
string protobuf_type = 2 [(buf.validate.field).required = true, (buf.validate.field).string.min_len = 1];
|
||||
}
|
||||
89
proto/audit/v1/routable_event.proto
Normal file
89
proto/audit/v1/routable_event.proto
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
syntax = "proto3";
|
||||
|
||||
import "buf/validate/validate.proto";
|
||||
|
||||
package audit.v1;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
enum ObjectName {
|
||||
OBJECT_NAME_UNSPECIFIED = 0;
|
||||
// If the action happens on system level and doesn't relate to a known ObjectType.
|
||||
OBJECT_NAME_SYSTEM = 1;
|
||||
}
|
||||
|
||||
// The type of the object the audit event refers to.
|
||||
// Relevant for type-detection and lookups in the routing.
|
||||
enum ObjectType {
|
||||
OBJECT_TYPE_UNSPECIFIED = 0;
|
||||
OBJECT_TYPE_ORGANIZATION = 1;
|
||||
OBJECT_TYPE_FOLDER = 2;
|
||||
OBJECT_TYPE_PROJECT = 3;
|
||||
}
|
||||
|
||||
message ObjectIdentifier {
|
||||
// Identifier of the respective entity (e.g. Identifier of an organization)
|
||||
string identifier = 1 [(buf.validate.field).required = true, (buf.validate.field).string.uuid = true];
|
||||
|
||||
// Type of the respective entity relevant for routing
|
||||
ObjectType type = 2 [(buf.validate.field).required = true, (buf.validate.field).enum.defined_only = true];
|
||||
}
|
||||
|
||||
message EncryptedData {
|
||||
// Encrypted serialized protobuf content (the actual audit event)
|
||||
bytes data = 1 [(buf.validate.field).required = true, (buf.validate.field).bytes.min_len = 1];
|
||||
|
||||
// Name of the protobuf type
|
||||
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
|
||||
string encrypted_password = 3 [(buf.validate.field).required = true, (buf.validate.field).string.min_len = 1];
|
||||
|
||||
// Version of the encrypted key
|
||||
int32 key_version = 4 [(buf.validate.field).int32.gte = 1];
|
||||
}
|
||||
|
||||
message UnencryptedData {
|
||||
// Unencrypted serialized protobuf content (the actual audit event)
|
||||
bytes data = 1 [(buf.validate.field).required = true, (buf.validate.field).bytes.min_len = 1];
|
||||
|
||||
// Name of the protobuf type
|
||||
string protobuf_type = 2 [(buf.validate.field).required = true, (buf.validate.field).string.min_len = 1];
|
||||
}
|
||||
|
||||
message RoutableAuditEvent {
|
||||
|
||||
// Functional event name with pattern <TYPE>_<ACTION>, e.g. ORGANIZATION_CREATED
|
||||
// Will be copied over by the SDK from the AuditEvent
|
||||
string event_name = 1 [(buf.validate.field).required = true, (buf.validate.field).string.pattern = "^[A-Z]+_[A-Z]+$"];
|
||||
|
||||
// Visibility relevant for differentiating between internal and public events
|
||||
Visibility visibility = 2 [(buf.validate.field).required = true, (buf.validate.field).enum.defined_only = true];
|
||||
|
||||
// Identifier the audit log event refers to
|
||||
oneof resource_reference {
|
||||
option (buf.validate.oneof).required = true;
|
||||
// If it is a technical event not related to an organization, folder or project
|
||||
// Will NOT be routed to the end-user, only for internal analysis ->
|
||||
// Clarify what do in the router
|
||||
ObjectName object_name = 3;
|
||||
ObjectIdentifier object_identifier = 4;
|
||||
}
|
||||
|
||||
// The actual audit event is transferred in one of the attributes below
|
||||
oneof data {
|
||||
option (buf.validate.oneof).required = true;
|
||||
UnencryptedData unencrypted_data = 5;
|
||||
EncryptedData encrypted_data = 6;
|
||||
}
|
||||
}
|
||||
34
proto/buf.gen.yaml
Normal file
34
proto/buf.gen.yaml
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
version: v2
|
||||
managed:
|
||||
enabled: true
|
||||
override:
|
||||
- file_option: java_package_prefix
|
||||
value: ""
|
||||
#override:
|
||||
# - file_option: go_package_prefix
|
||||
# <module_name> : name in go.mod
|
||||
# <relative_path> : where generated code should be output
|
||||
# value: dev.azure.com/schwarzit/schwarzit.stackit-core-platform/message-schema
|
||||
# Remove `disable` field if googleapis is not used
|
||||
# disable:
|
||||
# - module: buf.build/googleapis/googleapis
|
||||
# file_option: go_package_prefix
|
||||
plugins:
|
||||
- remote: buf.build/protocolbuffers/go:v1.34.1
|
||||
out: ../gen/go
|
||||
opt:
|
||||
- paths=source_relative
|
||||
- remote: buf.build/protocolbuffers/java:v25.3
|
||||
out: ../gen/java/schema-proto.jar
|
||||
- remote: buf.build/bufbuild/validate-go:v1.0.4
|
||||
out: ../gen/go
|
||||
opt:
|
||||
- paths=source_relative
|
||||
- remote: buf.build/bufbuild/validate-java:v1.0.4
|
||||
out: ../gen/java
|
||||
opt:
|
||||
- paths=source_relative
|
||||
- remote: buf.build/protocolbuffers/python:v23.4
|
||||
out: ../gen/python
|
||||
# - remote: buf.build/protocolbuffers/pyi
|
||||
# out: ../gen/python
|
||||
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: 46a4cf4ba1094a34bcd89a6c67163b4b
|
||||
digest: shake256:436ce453801917c11bc7b21d66bcfae87da2aceb804a041487be1e51dc9fbc219e61ea6a552db7a7aa6d63bb5efd0f3ed5fe3d4c42d4f750d0eb35f14144e3b6
|
||||
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:
|
||||
- DEFAULT
|
||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
protovalidate
|
||||
347
solace.go
Normal file
347
solace.go
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Azure/go-amqp"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
"log/slog"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
AmqpTopicPrefix = "topic://"
|
||||
AmqpQueuePrefix = "queue://"
|
||||
)
|
||||
|
||||
// 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 *Client
|
||||
}
|
||||
|
||||
// NewSolaceContainer starts a container and
|
||||
func NewSolaceContainer(ctx context.Context) (*SolaceContainer, error) {
|
||||
|
||||
env := make(map[string]string)
|
||||
env["username_admin_globalaccesslevel"] = "admin"
|
||||
env["username_admin_password"] = "admin"
|
||||
|
||||
// Start docker container
|
||||
request := testcontainers.ContainerRequest{
|
||||
Image: "solace/solace-pubsub-standard:10.8",
|
||||
ExposedPorts: []string{"5672/tcp", "8080/tcp"},
|
||||
SkipReaper: true,
|
||||
AutoRemove: true,
|
||||
ShmSize: 1024 * 1024 * 1024, // 1 GB,
|
||||
Env: env,
|
||||
WaitingFor: wait.ForLog("Running pre-startup checks:").
|
||||
WithStartupTimeout(60 * 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
|
||||
}
|
||||
slog.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 := NewClient(
|
||||
sempApiBaseUrl,
|
||||
true,
|
||||
false,
|
||||
BasicAuth("admin", "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
|
||||
} else {
|
||||
time.Sleep(100 * 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",
|
||||
fmt.Sprintf("/config/msgVpns/default/queues"), queueConfig)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// QueueExists checks if a queue with the given name exists.
|
||||
func (c SolaceContainer) QueueExists(ctx context.Context, queueName string) (bool, error) {
|
||||
|
||||
// Check if exists
|
||||
_, err := c.sempClient.RequestWithoutBody(
|
||||
ctx,
|
||||
"GET",
|
||||
fmt.Sprintf("/config/msgVpns/default/queues/%s", queueName),
|
||||
)
|
||||
// Check if response contains "NOT_FOUND" string indicating that the queue doesn't exist
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "NOT_FOUND") {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Return technical errors
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Return queue exists
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// QueueDeleteIfExists deletes the queue with the given name if it exists.
|
||||
func (c SolaceContainer) QueueDeleteIfExists(ctx context.Context, queueName string) error {
|
||||
|
||||
// Check if queue exists
|
||||
exists, err := c.QueueExists(ctx, queueName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete if exists
|
||||
if exists {
|
||||
_, err := c.sempClient.RequestWithoutBody(
|
||||
ctx,
|
||||
"DELETE",
|
||||
fmt.Sprintf("/config/msgVpns/default/queues/%s", queueName),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TopicSubscriptionCreate creates a topic subscription for a (underlying) queue.
|
||||
//
|
||||
// Parameters:
|
||||
// * ctx - the context object
|
||||
// * queueName - the name of the queue where the topic(s) should be subscribed
|
||||
// * topicName - the name of the topic with optional wildcards (e.g. "organizations/org-*")
|
||||
func (c SolaceContainer) TopicSubscriptionCreate(ctx context.Context, queueName string, topicName string) error {
|
||||
|
||||
// Construct url and parameters
|
||||
url := fmt.Sprintf("/config/msgVpns/default/queues/%s/subscriptions", queueName)
|
||||
|
||||
subscriptionConfig := make(map[string]any)
|
||||
subscriptionConfig["subscriptionTopic"] = topicName
|
||||
|
||||
// Create the subscription
|
||||
_, err := c.sempClient.RequestWithBody(ctx, "POST", url, subscriptionConfig)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c SolaceContainer) NewAmqpConnection(ctx context.Context) (*amqp.Conn, error) {
|
||||
return amqp.Dial(ctx, c.AmqpConnectionString, nil)
|
||||
}
|
||||
|
||||
// ValidateTopicName checks that topicName and topicSubscriptionTopicPattern are valid and compatible
|
||||
// Solace topic name constraints can be found here:
|
||||
// https://docs.solace.com/Messaging/SMF-Topics.htm
|
||||
func (c SolaceContainer) ValidateTopicName(topicSubscriptionTopicPattern string, topicName string) error {
|
||||
// Cut off the topic:// prefix
|
||||
var name string
|
||||
if strings.HasPrefix(topicName, "topic://") {
|
||||
name = topicName[len("topic://"):]
|
||||
} else {
|
||||
name = topicName
|
||||
}
|
||||
|
||||
// Check input
|
||||
if topicSubscriptionTopicPattern == "" {
|
||||
return errors.New("topicSubscriptionTopicPattern is empty")
|
||||
}
|
||||
if name == "" {
|
||||
return errors.New("topicName is empty")
|
||||
}
|
||||
|
||||
// Check topic name
|
||||
allowedTopicCharacters, err := regexp.Compile("[0-9A-Za-z-.]+(?:/[0-9A-Za-z-.]+)+|[0-9A-Za-z-.]+")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !allowedTopicCharacters.MatchString(name) {
|
||||
return errors.New("invalid topic name")
|
||||
}
|
||||
|
||||
// Check topic subscription topic pattern
|
||||
allowedTopicSubscriptionCharacters, err := regexp.Compile("(?:(?:[0-9A-Za-z-.]+|[0-9A-Za-z-.]*\\*)(?:/(?:[0-9A-Za-z-.]+|[0-9A-Za-z-.]*\\*))+|(?:[0-9A-Za-z-.]+|[0-9A-Za-z-.]*\\*)|/>)|>")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !allowedTopicSubscriptionCharacters.MatchString(topicSubscriptionTopicPattern) {
|
||||
return errors.New("invalid topic subscription name")
|
||||
}
|
||||
|
||||
// Check compatibility
|
||||
subscriptionIndex := 0
|
||||
var expectedNextCharacter uint8 = 0
|
||||
var nextError error
|
||||
for i := 0; i < len(name); i++ {
|
||||
if expectedNextCharacter != 0 {
|
||||
if expectedNextCharacter != name[i] {
|
||||
return nextError
|
||||
} else {
|
||||
expectedNextCharacter = 0
|
||||
nextError = nil
|
||||
}
|
||||
}
|
||||
|
||||
switch topicSubscriptionTopicPattern[subscriptionIndex] {
|
||||
case '*':
|
||||
if name[i] == '/' {
|
||||
expectedNextCharacter = '/'
|
||||
nextError = errors.New(fmt.Sprintf("Invalid character '/' at index %d", i))
|
||||
subscriptionIndex++
|
||||
}
|
||||
break
|
||||
case '/':
|
||||
if name[i] != '/' {
|
||||
return errors.New(fmt.Sprintf("Expected character '/', got %c at index %d", name[i], i))
|
||||
}
|
||||
subscriptionIndex++
|
||||
break
|
||||
case '>':
|
||||
// everything is allowed
|
||||
break
|
||||
default:
|
||||
if name[i] != topicSubscriptionTopicPattern[subscriptionIndex] {
|
||||
return errors.New(fmt.Sprintf(
|
||||
"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()
|
||||
}
|
||||
}
|
||||
186
test_data.go
Normal file
186
test_data.go
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
auditV1 "audit-schema/gen/go/audit/v1"
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewOrganizationAuditEvent(
|
||||
customization *func(
|
||||
*auditV1.AuditEvent,
|
||||
*RoutingIdentifier,
|
||||
*auditV1.ObjectIdentifier,
|
||||
)) (
|
||||
*auditV1.AuditEvent,
|
||||
*RoutingIdentifier,
|
||||
*auditV1.ObjectIdentifier,
|
||||
) {
|
||||
|
||||
auditEvent := &auditV1.AuditEvent{
|
||||
EventName: "ORGANIZATION_CREATED",
|
||||
EventTimeStamp: timestamppb.New(time.Now()),
|
||||
EventTrigger: auditV1.EventTrigger_EVENT_TRIGGER_EVENT,
|
||||
Initiator: &auditV1.Principal{
|
||||
Id: uuid.NewString(),
|
||||
},
|
||||
}
|
||||
|
||||
identifier := uuid.New()
|
||||
routingIdentifier := &RoutingIdentifier{
|
||||
Identifier: identifier,
|
||||
Type: RoutingIdentifierTypeOrganization,
|
||||
}
|
||||
|
||||
objectIdentifier := &auditV1.ObjectIdentifier{
|
||||
Identifier: identifier.String(),
|
||||
Type: auditV1.ObjectType_OBJECT_TYPE_ORGANIZATION,
|
||||
}
|
||||
|
||||
if customization != nil {
|
||||
(*customization)(auditEvent, routingIdentifier, objectIdentifier)
|
||||
}
|
||||
|
||||
return auditEvent, routingIdentifier, objectIdentifier
|
||||
}
|
||||
|
||||
func NewOrganizationAuditEventWithDetails() (*auditV1.AuditEvent,
|
||||
*RoutingIdentifier,
|
||||
*auditV1.ObjectIdentifier) {
|
||||
customization := func(event *auditV1.AuditEvent,
|
||||
routingIdentifier *RoutingIdentifier,
|
||||
objectIdentifier *auditV1.ObjectIdentifier) {
|
||||
userAgent := "firefox"
|
||||
parameters, _ := structpb.NewStruct(map[string]any{"parameter1": "value"})
|
||||
body, _ := structpb.NewStruct(map[string]any{"body": "value"})
|
||||
|
||||
event.Request = &auditV1.RequestDetails{
|
||||
Endpoint: "/test",
|
||||
SourceIpAddress: "127.0.0.1",
|
||||
UserAgent: &userAgent,
|
||||
Parameters: parameters,
|
||||
Body: body,
|
||||
Headers: []*auditV1.RequestHeader{
|
||||
{
|
||||
Key: "header1",
|
||||
Value: "value",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
email := "test@example.com"
|
||||
event.Principals = []*auditV1.Principal{
|
||||
{
|
||||
Id: "id",
|
||||
Email: &email,
|
||||
},
|
||||
}
|
||||
|
||||
details, _ := structpb.NewStruct(map[string]interface{}{
|
||||
"detail": "value",
|
||||
})
|
||||
event.Details = details
|
||||
|
||||
result, _ := structpb.NewStruct(map[string]interface{}{
|
||||
"result": "value",
|
||||
})
|
||||
event.Result = result
|
||||
}
|
||||
return NewOrganizationAuditEvent(&customization)
|
||||
}
|
||||
|
||||
func NewFolderAuditEvent(
|
||||
customization *func(
|
||||
*auditV1.AuditEvent,
|
||||
*RoutingIdentifier,
|
||||
*auditV1.ObjectIdentifier,
|
||||
)) (
|
||||
*auditV1.AuditEvent,
|
||||
*RoutingIdentifier,
|
||||
*auditV1.ObjectIdentifier,
|
||||
) {
|
||||
|
||||
auditEvent := &auditV1.AuditEvent{
|
||||
EventName: "FOLDER_CREATED",
|
||||
EventTimeStamp: timestamppb.New(time.Now()),
|
||||
EventTrigger: auditV1.EventTrigger_EVENT_TRIGGER_EVENT,
|
||||
Initiator: &auditV1.Principal{
|
||||
Id: uuid.NewString(),
|
||||
},
|
||||
}
|
||||
|
||||
routingIdentifier := &RoutingIdentifier{
|
||||
Identifier: uuid.New(),
|
||||
Type: RoutingIdentifierTypeOrganization,
|
||||
}
|
||||
|
||||
objectIdentifier := &auditV1.ObjectIdentifier{
|
||||
Identifier: uuid.New().String(),
|
||||
Type: auditV1.ObjectType_OBJECT_TYPE_FOLDER,
|
||||
}
|
||||
|
||||
if customization != nil {
|
||||
(*customization)(auditEvent, routingIdentifier, objectIdentifier)
|
||||
}
|
||||
|
||||
return auditEvent, routingIdentifier, objectIdentifier
|
||||
}
|
||||
|
||||
func NewProjectAuditEvent(
|
||||
customization *func(
|
||||
*auditV1.AuditEvent,
|
||||
*RoutingIdentifier,
|
||||
*auditV1.ObjectIdentifier,
|
||||
)) (
|
||||
*auditV1.AuditEvent,
|
||||
*RoutingIdentifier,
|
||||
*auditV1.ObjectIdentifier,
|
||||
) {
|
||||
|
||||
auditEvent := &auditV1.AuditEvent{
|
||||
EventName: "PROJECT_CREATED",
|
||||
EventTimeStamp: timestamppb.New(time.Now()),
|
||||
EventTrigger: auditV1.EventTrigger_EVENT_TRIGGER_EVENT,
|
||||
Initiator: &auditV1.Principal{
|
||||
Id: uuid.NewString(),
|
||||
},
|
||||
}
|
||||
|
||||
identifier := uuid.New()
|
||||
routingIdentifier := &RoutingIdentifier{
|
||||
Identifier: identifier,
|
||||
Type: RoutingIdentifierTypeProject,
|
||||
}
|
||||
|
||||
objectIdentifier := &auditV1.ObjectIdentifier{
|
||||
Identifier: identifier.String(),
|
||||
Type: auditV1.ObjectType_OBJECT_TYPE_PROJECT,
|
||||
}
|
||||
|
||||
if customization != nil {
|
||||
(*customization)(auditEvent, routingIdentifier, objectIdentifier)
|
||||
}
|
||||
|
||||
return auditEvent, routingIdentifier, objectIdentifier
|
||||
}
|
||||
|
||||
func NewSystemAuditEvent(
|
||||
customization *func(*auditV1.AuditEvent)) *auditV1.AuditEvent {
|
||||
|
||||
auditEvent := &auditV1.AuditEvent{
|
||||
EventName: "SYSTEM_CHANGED",
|
||||
EventTimeStamp: timestamppb.New(time.Now()),
|
||||
EventTrigger: auditV1.EventTrigger_EVENT_TRIGGER_EVENT,
|
||||
Initiator: &auditV1.Principal{
|
||||
Id: uuid.NewString(),
|
||||
},
|
||||
}
|
||||
|
||||
if customization != nil {
|
||||
(*customization)(auditEvent)
|
||||
}
|
||||
|
||||
return auditEvent
|
||||
}
|
||||
Loading…
Reference in a new issue