From 901990f2b857e890ad8cc58859276a36e04f3926 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 27 Aug 2026 19:16:09 +0800 Subject: [PATCH] feat(shared): add audit package Land Auditor, Event, and domain/action constants in shared/audit so downstream modules can depend on a tagged shared release. Co-authored-by: Cursor --- shared/README.md | 20 +++++++++++++ shared/audit/audit.go | 59 ++++++++++++++++++++++++++++++++++++++ shared/audit/audit_test.go | 59 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 shared/audit/audit.go create mode 100644 shared/audit/audit_test.go diff --git a/shared/README.md b/shared/README.md index 5ec470c..5617d50 100644 --- a/shared/README.md +++ b/shared/README.md @@ -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} +} +``` diff --git a/shared/audit/audit.go b/shared/audit/audit.go new file mode 100644 index 0000000..7306ced --- /dev/null +++ b/shared/audit/audit.go @@ -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) +} diff --git a/shared/audit/audit_test.go b/shared/audit/audit_test.go new file mode 100644 index 0000000..ed66ff7 --- /dev/null +++ b/shared/audit/audit_test.go @@ -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) }