83 lines
2.9 KiB
Go
83 lines
2.9 KiB
Go
|
|
package customschema
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"log/slog"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/google/uuid"
|
||
|
|
)
|
||
|
|
|
||
|
|
// AuditAction is the enum of auditable actions taken by customschema.Service.
|
||
|
|
type AuditAction string
|
||
|
|
|
||
|
|
const (
|
||
|
|
AuditActionSchemaCreate AuditAction = "schema.create"
|
||
|
|
AuditActionSchemaUpdate AuditAction = "schema.update"
|
||
|
|
AuditActionSchemaDelete AuditAction = "schema.delete"
|
||
|
|
AuditActionSchemaAssign AuditAction = "schema.assign"
|
||
|
|
AuditActionMetadataWrite AuditAction = "metadata.write"
|
||
|
|
AuditActionMetadataReset AuditAction = "metadata.reset"
|
||
|
|
AuditActionFolderAssignSchema AuditAction = "folder.assign_schema"
|
||
|
|
)
|
||
|
|
|
||
|
|
// AuditRecord captures one auditable event emitted by customschema.Service.
|
||
|
|
// Actor is a Cognito subject UUID (not an email). ClientID is the
|
||
|
|
// varchar(255) Cognito-pool-style client identifier (not a uuid) because
|
||
|
|
// that is how it is stored in the clients table and in every FK that
|
||
|
|
// points to it. Details holds any per-action fields that do not fit the
|
||
|
|
// structured columns above.
|
||
|
|
type AuditRecord struct {
|
||
|
|
Actor string
|
||
|
|
Action AuditAction
|
||
|
|
ResourceID uuid.UUID
|
||
|
|
ClientID string
|
||
|
|
Timestamp time.Time
|
||
|
|
Details map[string]any
|
||
|
|
}
|
||
|
|
|
||
|
|
// AuditSink is the narrow contract customschema.Service uses to record
|
||
|
|
// events. The default implementation wraps a *slog.Logger; tests can
|
||
|
|
// substitute an in-memory capturing sink.
|
||
|
|
type AuditSink interface {
|
||
|
|
Record(ctx context.Context, rec AuditRecord) error
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewSlogAuditSink returns a sink that writes every record to the provided
|
||
|
|
// logger with a fixed audit=true attribute so downstream log aggregation
|
||
|
|
// can filter audit events from general application logs.
|
||
|
|
//
|
||
|
|
// STUB — replaced by the golang engineer in the next dispatch. Currently
|
||
|
|
// returns a sink whose Record method does nothing but returns nil, so the
|
||
|
|
// tests compile and fail on behavior assertions (not on build errors).
|
||
|
|
func NewSlogAuditSink(logger *slog.Logger) AuditSink {
|
||
|
|
return &slogAuditSink{logger: logger}
|
||
|
|
}
|
||
|
|
|
||
|
|
type slogAuditSink struct {
|
||
|
|
logger *slog.Logger
|
||
|
|
}
|
||
|
|
|
||
|
|
// Record is the stub body. The real implementation must emit a single
|
||
|
|
// structured log event at info level with message "audit" and the
|
||
|
|
// following attributes:
|
||
|
|
//
|
||
|
|
// audit=true, action=<string>, actor=<string>, resource_id=<uuid>,
|
||
|
|
// client_id=<uuid>, timestamp=<RFC3339Nano>, plus every Details key as
|
||
|
|
// its own slog.Any attribute (merged flat onto the event).
|
||
|
|
func (s *slogAuditSink) Record(ctx context.Context, rec AuditRecord) error {
|
||
|
|
attrs := []slog.Attr{
|
||
|
|
slog.Bool("audit", true),
|
||
|
|
slog.String("action", string(rec.Action)),
|
||
|
|
slog.String("actor", rec.Actor),
|
||
|
|
slog.String("resource_id", rec.ResourceID.String()),
|
||
|
|
slog.String("client_id", rec.ClientID),
|
||
|
|
slog.String("timestamp", rec.Timestamp.Format(time.RFC3339Nano)),
|
||
|
|
}
|
||
|
|
for k, v := range rec.Details {
|
||
|
|
attrs = append(attrs, slog.Any(k, v))
|
||
|
|
}
|
||
|
|
s.logger.LogAttrs(ctx, slog.LevelInfo, "audit", attrs...)
|
||
|
|
return nil
|
||
|
|
}
|