wulf-pulse/migrations/102_route53_tables.sql

150 lines
8.1 KiB
MySQL
Raw Permalink Normal View History

-- AWS Route 53 DNS sync — Phase 24 dedicated Postgres schema.
--
-- Lays down the full Route 53 schema this phase's downstream plans (sync
-- service, CRUD routes, health check, admin UI) will populate and consume.
--
-- • Hosted zones (mirror of AWS Route 53 zones) -> route53_zones
-- • Resource record sets (mirror of AWS records) -> route53_records
-- • Append-only change ledger (drift + CRUD) -> route53_record_history
-- • Append-only attempt audit log (D-03/D-07) -> route53_audit_log
--
-- Retention is unbounded by design (D-08) — no purge job, no TTL, and no
-- DELETE statement against route53_record_history or route53_audit_log
-- anywhere in this phase, matching existing Pulse convention (itglue_writes,
-- itglue_asset_audits, phishing audit_events are all forever-retained too).
-- ---------------------------------------------------------------------------
-- 1. route53_zones — mirror of AWS Route 53 hosted zones
-- ---------------------------------------------------------------------------
-- id is the AWS hosted zone id with the '/hostedzone/' prefix stripped — the
-- natural stable identifier Route 53 already provides, no synthetic UUID
-- needed. authoritative_name_servers backs the D-12 NS-delegation health
-- check planned in 24-04.
CREATE TABLE IF NOT EXISTS route53_zones (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
comment TEXT,
private_zone BOOLEAN NOT NULL DEFAULT false,
record_count INTEGER NOT NULL DEFAULT 0,
authoritative_name_servers JSONB,
raw_payload JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
is_deleted BOOLEAN NOT NULL DEFAULT false,
deleted_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_route53_zones_is_deleted ON route53_zones(is_deleted);
CREATE INDEX IF NOT EXISTS idx_route53_zones_name ON route53_zones(name);
-- ---------------------------------------------------------------------------
-- 2. route53_records — mirror of AWS Route 53 resource record sets
-- ---------------------------------------------------------------------------
-- record_key is the composite string `zoneId:name:type:setIdentifier` (empty
-- string when there is no set identifier) — Route 53 recordsets are uniquely
-- identified by zone+name+type+SetIdentifier; there is no AWS-side record id
-- to use as a natural primary key.
CREATE TABLE IF NOT EXISTS route53_records (
record_key TEXT PRIMARY KEY,
zone_id TEXT NOT NULL REFERENCES route53_zones(id) ON DELETE CASCADE,
name TEXT NOT NULL,
type TEXT NOT NULL,
set_identifier TEXT,
ttl INTEGER,
resource_records JSONB,
alias_target JSONB,
raw_payload JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
is_deleted BOOLEAN NOT NULL DEFAULT false,
deleted_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_route53_records_zone ON route53_records(zone_id);
CREATE INDEX IF NOT EXISTS idx_route53_records_is_deleted ON route53_records(is_deleted);
CREATE INDEX IF NOT EXISTS idx_route53_records_name_type ON route53_records(zone_id, name, type);
-- ---------------------------------------------------------------------------
-- 3. route53_record_history — append-only change ledger (D-06)
-- ---------------------------------------------------------------------------
-- Written by BOTH the sync service (source='sync_detected_drift', when a
-- previously-synced record's live AWS value no longer matches Postgres) and
-- the CRUD routes (source='pulse_crud', on every successful write). Distinct
-- from route53_audit_log below: this table is a per-record timeline of
-- resolved changes; the audit log below is a per-attempt ledger including
-- failures. audit_log_id is a soft (non-FK) reference so a history row
-- survives independent of the audit log's own lifecycle.
CREATE TABLE IF NOT EXISTS route53_record_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
zone_id TEXT NOT NULL REFERENCES route53_zones(id) ON DELETE CASCADE,
record_key TEXT NOT NULL,
record_name TEXT NOT NULL,
record_type TEXT NOT NULL,
change_action TEXT NOT NULL CHECK (change_action IN ('create','update','delete')),
before_value JSONB,
after_value JSONB,
source TEXT NOT NULL CHECK (source IN ('pulse_crud','sync_detected_drift')),
changed_by_user_id TEXT REFERENCES "user"(id) ON DELETE SET NULL,
changed_by_email TEXT,
audit_log_id UUID, -- soft ref -> route53_audit_log(id), no hard FK
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_route53_record_history_record ON route53_record_history(record_key, changed_at DESC);
CREATE INDEX IF NOT EXISTS idx_route53_record_history_source ON route53_record_history(source);
CREATE INDEX IF NOT EXISTS idx_route53_record_history_zone ON route53_record_history(zone_id, changed_at DESC);
-- ---------------------------------------------------------------------------
-- 4. route53_audit_log — append-only attempt audit log (D-03/D-07)
-- ---------------------------------------------------------------------------
-- One row per attempted CRUD/sync operation, including failures. zone_id is
-- deliberately NOT an FK here (unlike route53_record_history above) — a
-- failed attempt against a zone that was never synced must still be
-- recordable. D-03 accepted tradeoff: destructive record operations execute
-- immediately with no staged approval gate; this table is the compensating
-- post-hoc traceability control (actor, timestamp, before/after for every
-- attempt), not a pre-write block.
CREATE TABLE IF NOT EXISTS route53_audit_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
operation TEXT NOT NULL CHECK (operation IN ('create','update','delete','sync')),
zone_id TEXT,
record_key TEXT,
record_name TEXT,
record_type TEXT,
before_value JSONB,
after_value JSONB,
performed_by_user_id TEXT REFERENCES "user"(id) ON DELETE SET NULL,
performed_by_email TEXT,
performed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ,
status TEXT NOT NULL CHECK (status IN ('pending','committed','failed')),
aws_change_id TEXT,
aws_change_status TEXT,
aws_response JSONB,
error_message TEXT
);
CREATE INDEX IF NOT EXISTS ix_route53_audit_log_record ON route53_audit_log(zone_id, record_key, performed_at DESC);
CREATE INDEX IF NOT EXISTS ix_route53_audit_log_status ON route53_audit_log(status);
COMMENT ON TABLE route53_record_history IS
'Append-only per-record change timeline. source distinguishes CRUD writes (pulse_crud) from sync-detected drift (sync_detected_drift). Unbounded retention by design (D-08) — no purge job.';
COMMENT ON TABLE route53_audit_log IS
'Append-only per-attempt audit log including failures. D-03 accepted tradeoff: destructive record ops execute immediately with no pre-write approval gate; this table is the post-hoc compensating control. Unbounded retention by design (D-08) — no purge job.';
-- ---------------------------------------------------------------------------
-- 5. integration_settings seed row (D-10)
-- ---------------------------------------------------------------------------
-- Display-only toggle on /admin/integrations — no sync/CRUD-blocking
-- behavior anywhere in this phase (unlike PAX8's disable-gates-fullSync
-- exception). Extends the existing seed list from migrations/081 rather
-- than editing that committed file.
INSERT INTO integration_settings (key, disabled) VALUES ('route53', false) ON CONFLICT (key) DO NOTHING;