Files

83 lines
2.9 KiB
Go
Raw Permalink Normal View History

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
}