feat(store): add Area admission and audit outbox [T-010]
This commit is contained in:
@@ -48,6 +48,7 @@ func TestPostgresDefaultAndMaximumQuota(t *testing.T) {
|
||||
}
|
||||
radar := device.Device{
|
||||
ID: "radar-default", TenantID: "tenant-default", SiteID: "site-default",
|
||||
AreaID: "area-default",
|
||||
SerialNumber: "radar-default", Name: "Radar", Modality: device.ModalityRadar,
|
||||
Capabilities: []device.Capability{device.CapabilityTelemetry},
|
||||
DesiredState: device.DesiredEnabled, ActualState: device.ActualPending,
|
||||
@@ -113,8 +114,8 @@ func TestPostgresProjectionFailureAndRollbackFailClosed(t *testing.T) {
|
||||
store, admin := openPostgresTestStore(t)
|
||||
ctx := context.Background()
|
||||
err := store.CreateDevice(ctx, videoDevice(1, "missing-tenant", "missing-site"))
|
||||
if !errors.Is(err, ErrQuotaProjectionUnavailable) {
|
||||
t.Fatalf("missing projection must fail closed, got %v", err)
|
||||
if !errors.Is(err, ErrAreaPolicyUnavailable) {
|
||||
t.Fatalf("missing Area projection must fail closed first, got %v", err)
|
||||
}
|
||||
|
||||
insertBellSite(t, admin, "tenant", "site", 2)
|
||||
@@ -285,12 +286,12 @@ func TestPostgresOpenRejectsOverprivilegedRuntimeRole(t *testing.T) {
|
||||
_, admin := openPostgresTestStore(t)
|
||||
ctx := context.Background()
|
||||
if _, err := admin.ExecContext(ctx,
|
||||
`GRANT UPDATE ON bell.site_quota_v1 TO yovision_t009_sense`); err != nil {
|
||||
`GRANT UPDATE ON bell.site_quota_v1 TO yovision_t010_sense`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
_, _ = admin.ExecContext(context.Background(),
|
||||
`REVOKE UPDATE ON bell.site_quota_v1 FROM yovision_t009_sense`)
|
||||
`REVOKE UPDATE ON bell.site_quota_v1 FROM yovision_t010_sense`)
|
||||
}()
|
||||
value, err := OpenPostgres(ctx, os.Getenv(postgresTestDSNEnv))
|
||||
if value != nil {
|
||||
@@ -302,6 +303,316 @@ func TestPostgresOpenRejectsOverprivilegedRuntimeRole(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgresAreaPolicyAllowsNonImagingAndDeniesImagingCreate(t *testing.T) {
|
||||
store, admin := openPostgresTestStore(t)
|
||||
ctx := context.Background()
|
||||
insertBellSite(t, admin, "tenant", "site", 2)
|
||||
if _, err := admin.ExecContext(ctx, `UPDATE bell.areas SET capture_policy = 'non_imaging_only'
|
||||
WHERE tenant_id = 'tenant' AND id = 'area-default'`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
blocked := videoDevice(1, "tenant", "site")
|
||||
blocked.DesiredState = device.DesiredDisabled
|
||||
err := store.CreateDevice(ctx, blocked)
|
||||
if !errors.Is(err, ErrAreaPolicyDenied) {
|
||||
t.Fatalf("disabled imaging create must still be denied, got %v", err)
|
||||
}
|
||||
|
||||
radar := device.Device{
|
||||
ID: "radar-001", TenantID: "tenant", SiteID: "site", AreaID: "area-default",
|
||||
SerialNumber: "radar-001", Name: "Radar", Modality: device.ModalityRadar,
|
||||
Capabilities: []device.Capability{device.CapabilityTelemetry},
|
||||
DesiredState: device.DesiredEnabled, ActualState: device.ActualPending,
|
||||
}
|
||||
if err := store.CreateDevice(ctx, radar); err != nil {
|
||||
t.Fatalf("non-imaging device must be allowed: %v", err)
|
||||
}
|
||||
stored, err := store.GetDevice(ctx, radar.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stored.AreaID != "area-default" || stored.AreaPolicySourceVersion != 2 {
|
||||
t.Fatalf("Area projection evidence was not stored: %+v", stored)
|
||||
}
|
||||
var devices, audits int
|
||||
if err := admin.QueryRowContext(ctx, `SELECT count(*) FROM sense.devices`).Scan(&devices); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := admin.QueryRowContext(ctx, `SELECT count(*) FROM sense.device_operation_outbox`).Scan(&audits); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if devices != 1 || audits != 1 {
|
||||
t.Fatalf("denied create left partial state: devices=%d audits=%d", devices, audits)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgresAreaProjectionMissingAndRollbackFailClosed(t *testing.T) {
|
||||
store, admin := openPostgresTestStore(t)
|
||||
ctx := context.Background()
|
||||
insertBellSite(t, admin, "tenant", "site", 2)
|
||||
|
||||
missing := videoDevice(1, "tenant", "site")
|
||||
missing.AreaID = "missing-area"
|
||||
if err := store.CreateDevice(ctx, missing); !errors.Is(err, ErrAreaPolicyUnavailable) {
|
||||
t.Fatalf("missing Area projection must fail closed, got %v", err)
|
||||
}
|
||||
if _, err := store.db.ExecContext(ctx, `INSERT INTO sense.area_policy_projection_state(
|
||||
tenant_id, site_id, area_id, source_version, synced_at
|
||||
) VALUES ('tenant', 'site', 'area-default', 99, clock_timestamp())`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.CreateDevice(ctx, videoDevice(2, "tenant", "site")); !errors.Is(err, ErrAreaPolicyInvalid) {
|
||||
t.Fatalf("Area source-version rollback must fail closed, got %v", err)
|
||||
}
|
||||
var devices, audits int
|
||||
if err := admin.QueryRowContext(ctx, `SELECT count(*) FROM sense.devices`).Scan(&devices); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := admin.QueryRowContext(ctx, `SELECT count(*) FROM sense.device_operation_outbox`).Scan(&audits); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if devices != 0 || audits != 0 {
|
||||
t.Fatalf("failed Area admissions persisted state: devices=%d audits=%d", devices, audits)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgresConcurrentAreaObservationIsMonotonic(t *testing.T) {
|
||||
store, admin := openPostgresTestStore(t)
|
||||
insertBellSite(t, admin, "tenant", "site", 2)
|
||||
ctx := context.Background()
|
||||
start := make(chan struct{})
|
||||
errorsFound := make(chan error, 2)
|
||||
var wait sync.WaitGroup
|
||||
for index := 1; index <= 2; index++ {
|
||||
wait.Add(1)
|
||||
go func(index int) {
|
||||
defer wait.Done()
|
||||
value := videoDevice(index, "tenant", "site")
|
||||
value.DesiredState = device.DesiredDisabled
|
||||
<-start
|
||||
errorsFound <- store.CreateDevice(ctx, value)
|
||||
}(index)
|
||||
}
|
||||
close(start)
|
||||
wait.Wait()
|
||||
close(errorsFound)
|
||||
for err := range errorsFound {
|
||||
if err != nil {
|
||||
t.Fatalf("concurrent Area observation failed: %v", err)
|
||||
}
|
||||
}
|
||||
var sourceVersion int64
|
||||
if err := admin.QueryRowContext(ctx, `SELECT source_version
|
||||
FROM sense.area_policy_projection_state
|
||||
WHERE tenant_id = 'tenant' AND site_id = 'site' AND area_id = 'area-default'`).
|
||||
Scan(&sourceVersion); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sourceVersion != 1 {
|
||||
t.Fatalf("concurrent observation recorded version %d", sourceVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgresAreaProjectionCannotCrossTenantBoundary(t *testing.T) {
|
||||
store, admin := openPostgresTestStore(t)
|
||||
ctx := context.Background()
|
||||
insertBellSite(t, admin, "tenant-a", "site", 2)
|
||||
insertBellSite(t, admin, "tenant-b", "site", 2)
|
||||
insertBellArea(t, admin, "tenant-b", "site", "private-area", "video_allowed")
|
||||
value := videoDevice(1, "tenant-a", "site")
|
||||
value.AreaID = "private-area"
|
||||
if err := store.CreateDevice(ctx, value); !errors.Is(err, ErrAreaPolicyUnavailable) {
|
||||
t.Fatalf("cross-tenant Area was not hidden as unavailable: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgresEnableRechecksAreaWithoutStoppingExistingDevice(t *testing.T) {
|
||||
store, admin := openPostgresTestStore(t)
|
||||
ctx := context.Background()
|
||||
insertBellSite(t, admin, "tenant", "site", 2)
|
||||
disabled := videoDevice(1, "tenant", "site")
|
||||
disabled.DesiredState = device.DesiredDisabled
|
||||
if err := store.CreateDevice(ctx, disabled); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.CreateDevice(ctx, videoDevice(2, "tenant", "site")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := admin.ExecContext(ctx, `UPDATE bell.areas SET capture_policy = 'non_imaging_only'
|
||||
WHERE tenant_id = 'tenant' AND id = 'area-default'`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.SetDesiredState(ctx, disabled.ID, device.DesiredEnabled); !errors.Is(err, ErrAreaPolicyDenied) {
|
||||
t.Fatalf("enable under non-imaging policy must be denied, got %v", err)
|
||||
}
|
||||
stillDisabled, err := store.GetDevice(ctx, disabled.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stillEnabled, err := store.GetDevice(ctx, "camera-002")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stillDisabled.DesiredState != device.DesiredDisabled ||
|
||||
stillEnabled.DesiredState != device.DesiredEnabled {
|
||||
t.Fatalf("policy change altered existing state: disabled=%s enabled=%s",
|
||||
stillDisabled.DesiredState, stillEnabled.DesiredState)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgresAuditContextRedactionAndNoopDesiredState(t *testing.T) {
|
||||
store, admin := openPostgresTestStore(t)
|
||||
insertBellSite(t, admin, "tenant", "site", 2)
|
||||
ctx := WithAuditContext(context.Background(), AuditContext{
|
||||
ActorType: AuditActorUser, ActorID: "operator-7", Reason: "approved change", TraceID: "trace-7",
|
||||
})
|
||||
value := videoDevice(1, "tenant", "site")
|
||||
if err := store.CreateDevice(ctx, value); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.SetDesiredState(ctx, value.ID, device.DesiredEnabled); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stored, err := store.GetDevice(ctx, value.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stored.Generation != 1 {
|
||||
t.Fatalf("no-op desired-state request changed generation to %d", stored.Generation)
|
||||
}
|
||||
rows, err := admin.QueryContext(ctx, `SELECT event_id, event_type, actor_type, actor_id,
|
||||
COALESCE(reason, ''), COALESCE(trace_id, ''), payload::text
|
||||
FROM sense.device_operation_outbox ORDER BY occurred_at, event_id`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var count int
|
||||
for rows.Next() {
|
||||
var eventID, eventType, actorType, actorID, reason, traceID, payload string
|
||||
if err := rows.Scan(&eventID, &eventType, &actorType, &actorID, &reason, &traceID, &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
count++
|
||||
if !strings.HasPrefix(eventID, "audit_") || len(eventID) != 38 {
|
||||
t.Fatalf("invalid audit event ID %q", eventID)
|
||||
}
|
||||
if actorType != "user" || actorID != "operator-7" || reason != "approved change" || traceID != "trace-7" {
|
||||
t.Fatalf("audit principal/context drift: %s/%s %s %s", actorType, actorID, reason, traceID)
|
||||
}
|
||||
for _, secret := range []string{value.EndpointRef, value.CredentialRef, value.PathName} {
|
||||
if strings.Contains(payload, secret) {
|
||||
t.Fatalf("audit payload leaked sensitive runtime data for %s", eventType)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != 2 {
|
||||
t.Fatalf("expected create and no-op audit facts, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgresOutboxFailureRollsBackAdmission(t *testing.T) {
|
||||
store, admin := openPostgresTestStore(t)
|
||||
ctx := context.Background()
|
||||
insertBellSite(t, admin, "tenant", "site", 2)
|
||||
if _, err := admin.ExecContext(ctx, `CREATE FUNCTION sense.t010_reject_outbox()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $function$
|
||||
BEGIN RAISE EXCEPTION 'synthetic outbox failure'; END
|
||||
$function$;
|
||||
CREATE TRIGGER t010_reject_outbox BEFORE INSERT ON sense.device_operation_outbox
|
||||
FOR EACH ROW EXECUTE FUNCTION sense.t010_reject_outbox()`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
_, _ = admin.ExecContext(context.Background(),
|
||||
`DROP TRIGGER IF EXISTS t010_reject_outbox ON sense.device_operation_outbox;
|
||||
DROP FUNCTION IF EXISTS sense.t010_reject_outbox()`)
|
||||
}()
|
||||
if err := store.CreateDevice(ctx, videoDevice(1, "tenant", "site")); err == nil {
|
||||
t.Fatal("synthetic Outbox failure did not reject device creation")
|
||||
}
|
||||
var devices, projections, audits int
|
||||
if err := admin.QueryRowContext(ctx, `SELECT count(*) FROM sense.devices`).Scan(&devices); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := admin.QueryRowContext(ctx, `SELECT count(*) FROM sense.area_policy_projection_state`).Scan(&projections); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := admin.QueryRowContext(ctx, `SELECT count(*) FROM sense.device_operation_outbox`).Scan(&audits); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if devices != 0 || projections != 0 || audits != 0 {
|
||||
t.Fatalf("Outbox failure left partial transaction: devices=%d projections=%d audits=%d",
|
||||
devices, projections, audits)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgresOutboxFailureRollsBackDesiredState(t *testing.T) {
|
||||
store, admin := openPostgresTestStore(t)
|
||||
ctx := context.Background()
|
||||
insertBellSite(t, admin, "tenant", "site", 2)
|
||||
value := videoDevice(1, "tenant", "site")
|
||||
value.DesiredState = device.DesiredDisabled
|
||||
if err := store.CreateDevice(ctx, value); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := admin.ExecContext(ctx, `CREATE FUNCTION sense.t010_reject_state_audit()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $function$
|
||||
BEGIN RAISE EXCEPTION 'synthetic state-audit failure'; END
|
||||
$function$;
|
||||
CREATE TRIGGER t010_reject_state_audit BEFORE INSERT ON sense.device_operation_outbox
|
||||
FOR EACH ROW EXECUTE FUNCTION sense.t010_reject_state_audit()`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
_, _ = admin.ExecContext(context.Background(),
|
||||
`DROP TRIGGER IF EXISTS t010_reject_state_audit ON sense.device_operation_outbox;
|
||||
DROP FUNCTION IF EXISTS sense.t010_reject_state_audit()`)
|
||||
}()
|
||||
if err := store.SetDesiredState(ctx, value.ID, device.DesiredEnabled); err == nil {
|
||||
t.Fatal("synthetic Outbox failure did not reject desired-state change")
|
||||
}
|
||||
stored, err := store.GetDevice(ctx, value.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stored.DesiredState != device.DesiredDisabled || stored.Generation != 1 {
|
||||
t.Fatalf("Outbox failure committed desired state: %+v", stored)
|
||||
}
|
||||
var audits int
|
||||
if err := admin.QueryRowContext(ctx, `SELECT count(*) FROM sense.device_operation_outbox`).Scan(&audits); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if audits != 1 {
|
||||
t.Fatalf("failed desired-state transaction changed Outbox count to %d", audits)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgresOpenRejectsAreaSourcePrivilege(t *testing.T) {
|
||||
_, admin := openPostgresTestStore(t)
|
||||
ctx := context.Background()
|
||||
if _, err := admin.ExecContext(ctx,
|
||||
`GRANT SELECT ON bell.areas TO yovision_t010_sense`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
_, _ = admin.ExecContext(context.Background(),
|
||||
`REVOKE SELECT ON bell.areas FROM yovision_t010_sense`)
|
||||
}()
|
||||
value, err := OpenPostgres(ctx, os.Getenv(postgresTestDSNEnv))
|
||||
if value != nil {
|
||||
_ = value.Close()
|
||||
t.Fatal("Area-source privilege was accepted")
|
||||
}
|
||||
if err == nil || !strings.Contains(err.Error(), "privilege boundary") {
|
||||
t.Fatalf("expected Area privilege-boundary error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func openPostgresTestStore(t *testing.T) (*Postgres, *sql.DB) {
|
||||
t.Helper()
|
||||
dsn := os.Getenv(postgresTestDSNEnv)
|
||||
@@ -318,10 +629,13 @@ func openPostgresTestStore(t *testing.T) (*Postgres, *sql.DB) {
|
||||
t.Fatal("connect PostgreSQL test administrator")
|
||||
}
|
||||
if _, err := admin.ExecContext(context.Background(), `TRUNCATE
|
||||
sense.device_operation_outbox,
|
||||
sense.device_capabilities,
|
||||
sense.reconcile_state,
|
||||
sense.devices,
|
||||
sense.site_quota_projection_state,
|
||||
sense.area_policy_projection_state,
|
||||
bell.areas,
|
||||
bell.sites CASCADE`); err != nil {
|
||||
admin.Close()
|
||||
t.Fatal(err)
|
||||
@@ -350,4 +664,18 @@ func insertBellSite(t *testing.T, admin *sql.DB, tenantID, siteID string, quota
|
||||
if _, err := admin.ExecContext(context.Background(), query, arguments...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
insertBellArea(t, admin, tenantID, siteID, "area-default", "video_allowed")
|
||||
}
|
||||
|
||||
func insertBellArea(
|
||||
t *testing.T,
|
||||
admin *sql.DB,
|
||||
tenantID, siteID, areaID, capturePolicy string,
|
||||
) {
|
||||
t.Helper()
|
||||
if _, err := admin.ExecContext(context.Background(), `INSERT INTO bell.areas(
|
||||
tenant_id, site_id, id, name, capture_policy
|
||||
) VALUES ($1, $2, $3, $4, $5)`, tenantID, siteID, areaID, "Test Area", capturePolicy); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user