Files
MikrotikManager/backend/drizzle/0037_server_probe_storage_v2.sql
T

227 lines
16 KiB
PL/PgSQL

-- Fresh-only enrollment of empty diagnostic tables. No historical backfill,
-- source-generation claim, public registration or automatic cleanup is granted.
BEGIN;
LOCK TABLE server_probe_executions,server_probe_execution_stages,server_probe_execution_quarantines,mcp_diagnostic_cancellations IN ACCESS EXCLUSIVE MODE;
DO $$ BEGIN
IF EXISTS(SELECT 1 FROM server_probe_executions)
OR EXISTS(SELECT 1 FROM server_probe_execution_stages)
OR EXISTS(SELECT 1 FROM server_probe_execution_quarantines)
OR EXISTS(SELECT 1 FROM mcp_diagnostic_cancellations)
THEN RAISE EXCEPTION 'nonempty legacy diagnostic history prevents fresh storage enrollment'; END IF;
IF EXISTS(SELECT 1 FROM pg_catalog.pg_class WHERE relname IN
('server_probe_storage_usage','server_probe_storage_admissions','server_probe_execution_payloads'))
THEN RAISE EXCEPTION 'unmarked diagnostic storage relation prevents fresh storage enrollment'; END IF;
IF NOT EXISTS(SELECT 1 FROM mcp_settings WHERE id=1 AND requests_retention_days BETWEEN 1 AND 365)
THEN RAISE EXCEPTION 'authoritative probe retention setting unavailable'; END IF;
END $$;
CREATE TABLE server_probe_storage_usage (
scope text PRIMARY KEY CHECK(scope='global' OR (scope LIKE 'owner:%' AND length(scope) BETWEEN 7 AND 206)),
admissions bigint NOT NULL DEFAULT 0 CHECK(admissions>=0),
reserved_files bigint NOT NULL DEFAULT 0 CHECK(reserved_files>=0 AND reserved_files<=admissions),
reserved_bytes bigint NOT NULL DEFAULT 0 CHECK(reserved_bytes>=0),
CHECK(reserved_bytes=admissions*32768+reserved_files*262144)
);
CREATE TABLE server_probe_storage_admissions (
receipt_id uuid PRIMARY KEY,
owner_id text NOT NULL CHECK(length(owner_id) BETWEEN 1 AND 200),
identity_kind text NOT NULL CHECK(identity_kind IN ('mcp','rest')), execution_id uuid NOT NULL,
binding_proof text NOT NULL CHECK(binding_proof ~ '^[a-f0-9]{64}$'),
authority_expires_at timestamptz NOT NULL,
claim_token uuid, claim_attempted_at timestamptz,
purged_at timestamptz, cancelled_at timestamptz, created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
retention_days integer NOT NULL CHECK(retention_days BETWEEN 1 AND 365),
safety_ms integer NOT NULL DEFAULT 60000 CHECK(safety_ms BETWEEN 60000 AND 86400000),
gc_not_before timestamptz NOT NULL DEFAULT 'infinity',
CHECK(gc_not_before>=authority_expires_at+make_interval(days=>retention_days)+safety_ms*interval '1 millisecond'),
UNIQUE(identity_kind,execution_id),
CHECK(claim_token IS NULL OR (claim_attempted_at IS NULL AND purged_at IS NULL AND cancelled_at IS NULL)),
CHECK(cancelled_at IS NULL OR (claim_token IS NULL AND claim_attempted_at IS NOT NULL))
);
CREATE INDEX server_probe_storage_admissions_owner_age ON server_probe_storage_admissions(owner_id,authority_expires_at,receipt_id);
CREATE TABLE server_probe_execution_payloads (
receipt_id uuid PRIMARY KEY REFERENCES server_probe_executions(receipt_id),
receipt_ciphertext text NOT NULL CHECK(octet_length(receipt_ciphertext) BETWEEN 1 AND 262144),
receipt_proof text NOT NULL CHECK(receipt_proof ~ '^[a-f0-9]{64}$')
);
INSERT INTO server_probe_storage_usage(scope,admissions,reserved_files,reserved_bytes) VALUES('global',0,0,0);
-- PostgreSQL drops table CHECKs dependent on this column together with it.
-- The locked empty-table guard precedes this column removal; no data is copied.
ALTER TABLE server_probe_executions DROP COLUMN receipt_ciphertext;
ALTER TABLE server_probe_executions ADD CONSTRAINT server_probe_storage_terminal_state CHECK(
(state='claimed' AND receipt_proof IS NULL AND finished_at IS NULL AND attempted_stages IS NULL AND completed_stages IS NULL)
OR(state<>'claimed' AND receipt_proof IS NOT NULL AND finished_at IS NOT NULL AND attempted_stages IS NOT NULL AND completed_stages IS NOT NULL));
-- Predicate only: it performs no DELETE. Every guarded exception below must
-- independently satisfy the persisted lifetime, actual current retention and
-- original terminal operation. The GUC alone never grants cleanup authority.
CREATE FUNCTION server_probe_storage_gc_allowed(id uuid) RETURNS boolean LANGUAGE plpgsql SET TimeZone='UTC' AS $$
DECLARE a server_probe_storage_admissions%ROWTYPE; activity timestamptz; current_retention integer; terminal_at timestamptz;
BEGIN
IF current_setting('mm.probe_storage_gc',true) IS DISTINCT FROM 'on' THEN RETURN false; END IF;
PERFORM pg_advisory_xact_lock(hashtextextended('server-probe-storage-v2',0));
SELECT * INTO a FROM server_probe_storage_admissions WHERE receipt_id=id FOR UPDATE;
IF NOT FOUND OR a.gc_not_before>=clock_timestamp() OR a.authority_expires_at>=clock_timestamp() THEN RETURN false; END IF;
SELECT settings.requests_retention_days INTO current_retention FROM mcp_settings settings WHERE settings.id=1;
IF current_retention IS NULL OR current_retention NOT BETWEEN 1 AND 365 THEN RETURN false; END IF;
terminal_at:=a.created_at;
IF a.identity_kind='mcp' THEN
SELECT GREATEST(o.finished_at,p.expires_at,COALESCE(cancellation.created_at,o.finished_at)) INTO terminal_at
FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN mcp_connections owner_connection ON owner_connection.id=o.connection_id
LEFT JOIN mcp_diagnostic_cancellations cancellation ON cancellation.operation_id=o.id
WHERE o.id=a.execution_id AND owner_connection.owner_id=a.owner_id AND p.operation_id='servers.probes.run'
AND o.state IN ('applied','failed','unknown','rejected','partial','cancelled') AND o.finished_at IS NOT NULL
AND (cancellation.operation_id IS NULL OR(cancellation.connection_id=o.connection_id AND cancellation.actor_id=a.owner_id))
FOR UPDATE OF o,p,owner_connection;
IF NOT FOUND OR terminal_at IS NULL THEN RETURN false; END IF;
END IF;
SELECT GREATEST(a.created_at,a.authority_expires_at,terminal_at,COALESCE(a.cancelled_at,a.created_at),
COALESCE((SELECT finished_at FROM server_probe_executions WHERE receipt_id=id),a.created_at),
COALESCE((SELECT created_at FROM server_probe_execution_quarantines WHERE receipt_id=id),a.created_at),
COALESCE((SELECT max(GREATEST(created_at,COALESCE(received_at,created_at))) FROM server_probe_execution_stages WHERE receipt_id=id),a.created_at)) INTO activity;
RETURN activity+make_interval(days=>GREATEST(a.retention_days,current_retention))+a.safety_ms*interval '1 millisecond'<clock_timestamp();
END $$;
CREATE FUNCTION server_probe_storage_admission_guard() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE current_retention integer; activity timestamptz;
BEGIN
PERFORM pg_advisory_xact_lock(hashtextextended('server-probe-storage-v2',0));
IF TG_OP='DELETE' THEN
IF server_probe_storage_gc_allowed(OLD.receipt_id) THEN RETURN OLD; END IF;
RAISE EXCEPTION 'probe admission lifetime has not elapsed';
END IF;
SELECT requests_retention_days INTO current_retention FROM mcp_settings WHERE id=1;
IF current_retention IS NULL OR current_retention NOT BETWEEN 1 AND 365 THEN RAISE EXCEPTION 'probe retention unavailable'; END IF;
IF TG_OP='INSERT' THEN
IF NEW.purged_at IS NOT NULL OR (NEW.cancelled_at IS NULL AND
(NEW.authority_expires_at<=clock_timestamp() OR NEW.claim_token IS NULL OR NEW.claim_attempted_at IS NOT NULL))
OR (NEW.cancelled_at IS NOT NULL AND (NEW.claim_token IS NOT NULL OR NEW.claim_attempted_at IS NULL))
THEN RAISE EXCEPTION 'invalid fresh probe admission'; END IF;
IF NEW.cancelled_at IS NOT NULL THEN NEW.cancelled_at:=clock_timestamp(); NEW.claim_attempted_at:=NEW.cancelled_at; END IF;
NEW.retention_days:=current_retention; RETURN NEW;
END IF;
IF (to_jsonb(OLD)-ARRAY['claim_token','claim_attempted_at','purged_at','cancelled_at']) IS DISTINCT FROM
(to_jsonb(NEW)-ARRAY['claim_token','claim_attempted_at','purged_at','cancelled_at'])
THEN RAISE EXCEPTION 'immutable probe admission identity'; END IF;
IF OLD.cancelled_at IS DISTINCT FROM NEW.cancelled_at THEN
IF OLD.cancelled_at IS NULL AND NEW.cancelled_at IS NOT NULL AND NEW.claim_token IS NULL
AND NEW.purged_at IS NOT DISTINCT FROM OLD.purged_at AND NEW.claim_attempted_at IS NOT NULL
AND (OLD.claim_attempted_at IS NULL OR NEW.claim_attempted_at IS NOT DISTINCT FROM OLD.claim_attempted_at)
THEN NEW.cancelled_at:=clock_timestamp(); IF OLD.claim_attempted_at IS NULL THEN NEW.claim_attempted_at:=NEW.cancelled_at; END IF; RETURN NEW; END IF;
RAISE EXCEPTION 'probe cancellation is irreversible';
END IF;
IF OLD.purged_at IS NULL AND NEW.purged_at IS NULL AND OLD.claim_token IS NOT NULL AND NEW.claim_token IS NULL
AND OLD.cancelled_at IS NULL AND OLD.claim_attempted_at IS NULL AND NEW.claim_attempted_at IS NOT NULL AND OLD.authority_expires_at>clock_timestamp()
THEN RETURN NEW; END IF;
IF OLD.purged_at IS NULL AND NEW.purged_at IS NOT NULL AND NEW.claim_token IS NULL
AND NEW.claim_attempted_at IS NOT DISTINCT FROM OLD.claim_attempted_at THEN
SELECT GREATEST(OLD.authority_expires_at,COALESCE(e.finished_at,OLD.authority_expires_at),COALESCE(q.created_at,OLD.authority_expires_at)) INTO activity
FROM (SELECT 1) x LEFT JOIN server_probe_executions e ON e.receipt_id=OLD.receipt_id
LEFT JOIN server_probe_execution_quarantines q ON q.receipt_id=OLD.receipt_id;
IF activity<clock_timestamp()-make_interval(days=>GREATEST(OLD.retention_days,current_retention)) THEN RETURN NEW; END IF;
END IF;
RAISE EXCEPTION 'probe admission transition refused';
END $$;
CREATE TRIGGER server_probe_storage_admission_guard BEFORE INSERT OR UPDATE OR DELETE ON server_probe_storage_admissions
FOR EACH ROW EXECUTE FUNCTION server_probe_storage_admission_guard();
CREATE FUNCTION server_probe_storage_anchor_guard() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE a server_probe_storage_admissions%ROWTYPE;
BEGIN
PERFORM pg_advisory_xact_lock(hashtextextended('server-probe-storage-v2',0));
SELECT * INTO a FROM server_probe_storage_admissions WHERE receipt_id=NEW.receipt_id FOR UPDATE;
IF NOT FOUND OR a.owner_id<>NEW.actor_id OR a.identity_kind<>NEW.identity_kind OR a.execution_id<>NEW.execution_id
OR a.binding_proof<>NEW.binding_proof OR a.authority_expires_at<>NEW.authority_expires_at
OR a.purged_at IS NOT NULL OR a.cancelled_at IS NOT NULL OR a.claim_token IS NOT NULL OR a.claim_attempted_at IS NULL OR a.authority_expires_at<=clock_timestamp()
THEN RAISE EXCEPTION 'probe anchor has no live consumed admission'; END IF;
RETURN NEW;
END $$;
CREATE TRIGGER server_probe_storage_anchor_guard BEFORE INSERT OR UPDATE ON server_probe_executions
FOR EACH ROW EXECUTE FUNCTION server_probe_storage_anchor_guard();
CREATE OR REPLACE FUNCTION server_probe_execution_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
IF TG_OP='DELETE' THEN
IF server_probe_storage_gc_allowed(OLD.receipt_id) THEN RETURN OLD; END IF;
RAISE EXCEPTION 'probe execution lifetime has not elapsed';
END IF;
IF OLD.state<>'claimed' OR (to_jsonb(OLD)-ARRAY['state','attempted_stages','completed_stages','receipt_proof','finished_at']) IS DISTINCT FROM
(to_jsonb(NEW)-ARRAY['state','attempted_stages','completed_stages','receipt_proof','finished_at']) OR NEW.state='claimed'
THEN RAISE EXCEPTION 'probe execution identity or terminal receipt is immutable'; END IF;
IF EXISTS(SELECT 1 FROM server_probe_execution_quarantines WHERE receipt_id=OLD.receipt_id)
THEN RAISE EXCEPTION 'probe execution quarantined'; END IF;
RETURN NEW; END $$;
CREATE FUNCTION server_probe_storage_stage_guard() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
PERFORM pg_advisory_xact_lock(hashtextextended('server-probe-storage-v2',0));
PERFORM receipt_id FROM server_probe_storage_admissions WHERE receipt_id=NEW.receipt_id AND purged_at IS NULL AND cancelled_at IS NULL
AND claim_token IS NULL AND claim_attempted_at IS NOT NULL AND authority_expires_at>clock_timestamp() FOR UPDATE;
IF NOT FOUND THEN RAISE EXCEPTION 'probe stage has no live admission'; END IF;
RETURN NEW; END $$;
CREATE TRIGGER server_probe_storage_stage_guard BEFORE INSERT OR UPDATE ON server_probe_execution_stages
FOR EACH ROW EXECUTE FUNCTION server_probe_storage_stage_guard();
CREATE FUNCTION server_probe_storage_quarantine_guard() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
PERFORM pg_advisory_xact_lock(hashtextextended('server-probe-storage-v2',0));
PERFORM receipt_id FROM server_probe_storage_admissions WHERE receipt_id=NEW.receipt_id AND identity_kind=NEW.identity_kind
AND execution_id=NEW.execution_id AND binding_proof=NEW.binding_proof FOR UPDATE;
IF NOT FOUND THEN RAISE EXCEPTION 'probe quarantine has no committed matching admission'; END IF;
RETURN NEW; END $$;
CREATE TRIGGER server_probe_storage_quarantine_guard BEFORE INSERT ON server_probe_execution_quarantines
FOR EACH ROW EXECUTE FUNCTION server_probe_storage_quarantine_guard();
CREATE FUNCTION server_probe_storage_payload_guard() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE a server_probe_storage_admissions%ROWTYPE; e server_probe_executions%ROWTYPE;
BEGIN
PERFORM pg_advisory_xact_lock(hashtextextended('server-probe-storage-v2',0));
IF TG_OP='UPDATE' THEN RAISE EXCEPTION 'probe payload is immutable'; END IF;
IF TG_OP='DELETE' THEN
PERFORM receipt_id FROM server_probe_storage_admissions WHERE receipt_id=OLD.receipt_id AND purged_at IS NOT NULL FOR UPDATE;
IF NOT FOUND AND NOT server_probe_storage_gc_allowed(OLD.receipt_id) THEN RAISE EXCEPTION 'probe payload is not purged'; END IF;
RETURN OLD;
END IF;
SELECT * INTO a FROM server_probe_storage_admissions WHERE receipt_id=NEW.receipt_id FOR UPDATE;
IF NOT FOUND OR a.purged_at IS NOT NULL OR a.cancelled_at IS NOT NULL OR a.claim_token IS NOT NULL OR a.claim_attempted_at IS NULL OR a.authority_expires_at<=clock_timestamp()
THEN RAISE EXCEPTION 'probe payload has no live admission'; END IF;
SELECT * INTO e FROM server_probe_executions WHERE receipt_id=NEW.receipt_id FOR UPDATE;
IF NOT FOUND OR e.state='claimed' OR e.receipt_proof<>NEW.receipt_proof OR e.actor_id<>a.owner_id OR e.binding_proof<>a.binding_proof
OR e.identity_kind<>a.identity_kind OR e.execution_id<>a.execution_id OR EXISTS(SELECT 1 FROM server_probe_execution_quarantines q
WHERE q.receipt_id=NEW.receipt_id OR(q.identity_kind=a.identity_kind AND q.execution_id=a.execution_id))
THEN RAISE EXCEPTION 'probe payload terminal identity mismatch or quarantine'; END IF;
RETURN NEW;
END $$;
CREATE TRIGGER server_probe_storage_payload_guard BEFORE INSERT OR UPDATE OR DELETE ON server_probe_execution_payloads
FOR EACH ROW EXECUTE FUNCTION server_probe_storage_payload_guard();
-- Preserve every original INSERT/UPDATE rule; only the already-validated
-- lifetime predicate can admit DELETE; no cleanup is scheduled by migration.
CREATE OR REPLACE FUNCTION server_probe_stage_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
IF TG_OP='DELETE' THEN
IF server_probe_storage_gc_allowed(OLD.receipt_id) THEN RETURN OLD; END IF;
RAISE EXCEPTION 'probe stage lifetime has not elapsed';
END IF;
PERFORM receipt_id FROM server_probe_executions WHERE receipt_id=NEW.receipt_id AND state='claimed' FOR UPDATE;
IF NOT FOUND OR EXISTS(SELECT 1 FROM server_probe_execution_quarantines WHERE receipt_id=NEW.receipt_id) THEN RAISE EXCEPTION 'probe attempt is not executable'; END IF;
IF TG_OP='INSERT' AND (NEW.stage_index<>(SELECT COALESCE(max(stage_index),0)+1 FROM server_probe_execution_stages WHERE receipt_id=NEW.receipt_id)
OR EXISTS(SELECT 1 FROM server_probe_execution_stages WHERE receipt_id=NEW.receipt_id AND state<>'received'))
THEN RAISE EXCEPTION 'probe stages must be serial and contiguous'; END IF;
IF TG_OP='UPDATE' AND (OLD.state<>'intent' OR NEW.state<>'received' OR (to_jsonb(OLD)-ARRAY['state','outcome','received_at']) IS DISTINCT FROM (to_jsonb(NEW)-ARRAY['state','outcome','received_at']))
THEN RAISE EXCEPTION 'probe stage terminal state is immutable'; END IF;
RETURN NEW; END $$;
CREATE OR REPLACE FUNCTION server_probe_quarantine_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
IF TG_OP='DELETE' AND server_probe_storage_gc_allowed(OLD.receipt_id) THEN RETURN OLD; END IF;
RAISE EXCEPTION 'probe quarantine remains immutable'; END $$;
CREATE OR REPLACE FUNCTION mcp_diagnostic_cancellation_immutable() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE original_receipt uuid;
BEGIN
IF TG_OP='DELETE' THEN
SELECT a.receipt_id INTO original_receipt FROM server_probe_storage_admissions a
JOIN mcp_operations o ON o.id=a.execution_id AND o.connection_id=OLD.connection_id
WHERE a.identity_kind='mcp' AND a.execution_id=OLD.operation_id AND a.owner_id=OLD.actor_id;
IF FOUND AND server_probe_storage_gc_allowed(original_receipt) THEN RETURN OLD; END IF;
END IF;
RAISE EXCEPTION 'diagnostic cancellation lifetime has not elapsed';
END $$;
INSERT INTO schema_migrations(id) VALUES ('0037_server_probe_storage_v2') ON CONFLICT (id) DO NOTHING;
COMMIT;