Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions shared/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,23 @@ Shared validation helpers for config structs.
err := validation.TCPPort("Port", cfg.Port) // 1-65535
err := validation.HTTPURL("Endpoint", cfg.URL) // absolute http(s) URL
```

## `audit`

Shared auditor callback and event shape used by domain services. `Event`
carries Argus-aligned fields (`Action`, `Status`, actor/target, …).
Domain-specific payloads live on `Details` in the emitting package, not on
`Event` itself.

```go
type sink struct{}
func (sink) Audit(ctx context.Context, e audit.Event) { /* persist e */ }

type paymentDetails struct {
GatewayID string
Reference string
}
func (d paymentDetails) Metadata() map[string]any {
return map[string]any{"gateway_id": d.GatewayID, "reference": d.Reference}
}
```
59 changes: 59 additions & 0 deletions shared/audit/audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Lanka Software Foundation

// Package audit defines a domain-agnostic auditor callback and event shape.
// Domain-specific payloads live in the emitting package as Details implementations.
package audit

import (
"context"
"time"
)

// Action is a CRUD operation, matching Argus audit-log actions.
type Action string

const (
ActionCreate Action = "CREATE"
ActionRead Action = "READ"
ActionUpdate Action = "UPDATE"
ActionDelete Action = "DELETE"
)

// Status is the outcome of an audited operation, matching Argus audit-log status.
type Status string

const (
StatusSuccess Status = "SUCCESS"
StatusFailure Status = "FAILURE"
)

// Details is a domain-owned payload attached to an Event. Each service
// (payment, storage, …) defines its own type that implements Metadata.
// Nil Details means the event has nothing extra to attach.
type Details interface {
Metadata() map[string]any
}

// Event is a domain-agnostic audit record. Fields line up with
// github.com/LSFLK/argus/pkg/audit.AuditLogRequest so a later bridge is a
// straight field copy (Details.Metadata() becomes Metadata).
type Event struct {
TraceID string
Timestamp time.Time
EventType string
Action Action
Status Status
ActorType string
ActorID string
TargetType string
TargetID string

Details Details
}

// Auditor is an optional callback that services use to emit audit events.
// Implementations must be safe to call from any goroutine.
type Auditor interface {
Audit(ctx context.Context, e Event)
}
59 changes: 59 additions & 0 deletions shared/audit/audit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Lanka Software Foundation

package audit

import (
"context"
"testing"
"time"
)

type sampleDetails struct {
Key string
}

func (d sampleDetails) Metadata() map[string]any {
return map[string]any{"key": d.Key}
}

func TestEvent_DetailsOwnedByEmitter(t *testing.T) {
var recorded Event
auditor := AuditorFunc(func(_ context.Context, e Event) { recorded = e })

auditor.Audit(context.Background(), Event{
EventType: "STORAGE",
Action: ActionDelete,
Status: StatusSuccess,
TargetType: "RESOURCE",
TargetID: "abc",
Timestamp: time.Unix(1, 0).UTC(),
Details: sampleDetails{Key: "abc"},
})

if recorded.Action != ActionDelete || recorded.Status != StatusSuccess {
t.Fatalf("got action=%s status=%s", recorded.Action, recorded.Status)
}
d, ok := recorded.Details.(sampleDetails)
if !ok || d.Key != "abc" {
t.Fatalf("Details should round-trip as the emitter's type, got %#v", recorded.Details)
}
if recorded.Details.Metadata()["key"] != "abc" {
t.Fatalf("Metadata() = %v", recorded.Details.Metadata())
}
}

func TestEvent_NilDetails(t *testing.T) {
e := Event{Action: ActionRead, Status: StatusFailure}
if e.Details != nil {
t.Fatal("zero Event must have nil Details")
}
if e.Action != ActionRead || e.Status != StatusFailure {
t.Fatalf("got action=%s status=%s", e.Action, e.Status)
}
}

// AuditorFunc adapts a function to Auditor for tests.
type AuditorFunc func(context.Context, Event)

func (f AuditorFunc) Audit(ctx context.Context, e Event) { f(ctx, e) }
Loading