feat(store): add Area admission and audit outbox [T-010]
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AuditActorType string
|
||||
|
||||
const (
|
||||
AuditActorUser AuditActorType = "user"
|
||||
AuditActorService AuditActorType = "service"
|
||||
AuditActorSystem AuditActorType = "system"
|
||||
)
|
||||
|
||||
// AuditContext is supplied by a verified caller boundary. Repository callers
|
||||
// without an HTTP principal use the explicit system/sense fallback.
|
||||
type AuditContext struct {
|
||||
ActorType AuditActorType
|
||||
ActorID string
|
||||
Reason string
|
||||
TraceID string
|
||||
}
|
||||
|
||||
type auditContextKey struct{}
|
||||
|
||||
func WithAuditContext(ctx context.Context, value AuditContext) context.Context {
|
||||
return context.WithValue(ctx, auditContextKey{}, value)
|
||||
}
|
||||
|
||||
func auditFromContext(ctx context.Context) AuditContext {
|
||||
value, ok := ctx.Value(auditContextKey{}).(AuditContext)
|
||||
if !ok {
|
||||
return AuditContext{ActorType: AuditActorSystem, ActorID: "sense"}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func validateAudit(value AuditContext) error {
|
||||
if value.ActorType != AuditActorUser && value.ActorType != AuditActorService && value.ActorType != AuditActorSystem {
|
||||
return errors.New("invalid audit actor type")
|
||||
}
|
||||
if strings.TrimSpace(value.ActorID) == "" || len(value.ActorID) > 200 {
|
||||
return errors.New("invalid audit actor ID")
|
||||
}
|
||||
if len(value.Reason) > 500 {
|
||||
return errors.New("audit reason is too long")
|
||||
}
|
||||
if len(value.TraceID) > 128 {
|
||||
return errors.New("audit trace ID is too long")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type postgresAuditEvent struct {
|
||||
EventType string
|
||||
TenantID string
|
||||
SiteID string
|
||||
DeviceID string
|
||||
Generation int64
|
||||
QuotaSourceVersion int64
|
||||
AreaPolicySourceVersion int64
|
||||
Payload any
|
||||
OccurredAt time.Time
|
||||
}
|
||||
|
||||
func insertPostgresAudit(
|
||||
ctx context.Context,
|
||||
tx *sql.Tx,
|
||||
event postgresAuditEvent,
|
||||
) error {
|
||||
audit := auditFromContext(ctx)
|
||||
if err := validateAudit(audit); err != nil {
|
||||
return err
|
||||
}
|
||||
eventID, err := newAuditEventID()
|
||||
if err != nil {
|
||||
return errors.New("generate audit event ID")
|
||||
}
|
||||
payload, err := json.Marshal(event.Payload)
|
||||
if err != nil {
|
||||
return errors.New("encode audit payload")
|
||||
}
|
||||
if !json.Valid(payload) {
|
||||
return errors.New("invalid audit payload")
|
||||
}
|
||||
occurredAt := event.OccurredAt.UTC()
|
||||
if occurredAt.IsZero() {
|
||||
occurredAt = time.Now().UTC()
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `INSERT INTO sense.device_operation_outbox(
|
||||
event_id, event_type, tenant_id, site_id, device_id,
|
||||
actor_type, actor_id, reason, trace_id, aggregate_generation,
|
||||
quota_source_version, area_policy_source_version, payload,
|
||||
occurred_at, available_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, NULLIF($8, ''), NULLIF($9, ''),
|
||||
$10, $11, $12, $13, $14, $14)`,
|
||||
eventID, event.EventType, event.TenantID, event.SiteID, event.DeviceID,
|
||||
audit.ActorType, audit.ActorID, audit.Reason, audit.TraceID,
|
||||
event.Generation, nullableVersion(event.QuotaSourceVersion),
|
||||
nullableVersion(event.AreaPolicySourceVersion), payload, occurredAt)
|
||||
if err != nil {
|
||||
return errors.New("insert device operation audit")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newAuditEventID() (string, error) {
|
||||
value := make([]byte, 16)
|
||||
if _, err := rand.Read(value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "audit_" + hex.EncodeToString(value), nil
|
||||
}
|
||||
|
||||
func nullableVersion(value int64) any {
|
||||
if value < 1 {
|
||||
return nil
|
||||
}
|
||||
return value
|
||||
}
|
||||
Reference in New Issue
Block a user