-- Copyright (c) Microsoft Corporation.
-- Licensed under the PostgreSQL License.

-- 17_superuser_guc.sql
-- Tests: pg_durable.enable_superuser_instances GUC enforcement
--
-- This test runs in the "superuser-guc-off" phase where the server was started
-- WITHOUT pg_durable.enable_superuser_instances (defaults to off).  Because the
-- GUC is Postmaster-level, it cannot be changed at runtime.
--
-- Cases covered:
--   1. GUC off: superuser df.start() is rejected immediately.
--   2. Non-superuser unaffected: df_e2e_user can submit with GUC off.
--   3. Forgery caught by load_function_graph (instance-level rejection).
--   4. Forgery caught by execute_sql (node-level rejection, post-cache tamper).
--   5. Cross-iteration forgery neutralized by the frozen graph snapshot.
--
-- "GUC on + superuser succeeds" is implicitly covered by every other E2E test
-- (standard phase runs as postgres with enable_superuser_instances = on).
--
-- Runs as postgres throughout (superuser); identity switching is explicit.

-- ============================================================
-- Setup
-- ============================================================
DO $setup$
BEGIN
    -- Clean up forger role from previous runs
    PERFORM pg_terminate_backend(pid)
      FROM pg_stat_activity
      WHERE usename = 'su_guc_forger' AND pid <> pg_backend_pid();

    BEGIN DROP OWNED BY su_guc_forger; EXCEPTION WHEN undefined_object THEN NULL; END;
    BEGIN DROP ROLE su_guc_forger;     EXCEPTION WHEN undefined_object THEN NULL; END;
END $setup$;

-- Role with BYPASSRLS but NOT a superuser — the threat actor.
CREATE ROLE su_guc_forger LOGIN BYPASSRLS;
SELECT df.grant_usage('su_guc_forger');
GRANT TEMPORARY ON DATABASE postgres TO su_guc_forger;
-- The forger needs UPDATE on submitted_by to simulate the attack.
-- df.grant_usage() only grants UPDATE (status, updated_at).
GRANT UPDATE (submitted_by) ON df.instances TO su_guc_forger;
GRANT UPDATE (submitted_by) ON df.nodes TO su_guc_forger;

-- ============================================================
-- Test 1: GUC off — superuser df.start() is rejected
-- ============================================================
DO $$
DECLARE
    caught BOOLEAN := false;
    msg TEXT;
BEGIN
    BEGIN
        PERFORM df.start('SELECT 1', 'su-guc-test1');
    EXCEPTION WHEN OTHERS THEN
        caught := true;
        msg := SQLERRM;
    END;

    IF NOT caught THEN
        RAISE EXCEPTION 'TEST 1 FAILED: expected df.start() to raise an error for superuser when GUC is off';
    END IF;

    IF msg NOT LIKE '%enable_superuser_instances%' THEN
        RAISE EXCEPTION 'TEST 1 FAILED: error message does not mention enable_superuser_instances, got: %', msg;
    END IF;

    RAISE NOTICE 'TEST 1 PASSED: superuser df.start() rejected when GUC is off (error: %)', msg;
END $$;

-- ============================================================
-- Test 2: Non-superuser unaffected when GUC is off
-- ============================================================
SET SESSION AUTHORIZATION df_e2e_user;
CREATE TEMP TABLE _su_guc_t2 (instance_id TEXT);
INSERT INTO _su_guc_t2 SELECT df.start('SELECT 1', 'su-guc-test2');
RESET SESSION AUTHORIZATION;

DO $$
DECLARE
    inst_id TEXT;
    status TEXT;
BEGIN
    SELECT instance_id INTO inst_id FROM _su_guc_t2;
    SELECT df.await_instance(inst_id, 30) INTO status;
    IF status != 'completed' THEN
        RAISE EXCEPTION 'TEST 2 FAILED: non-superuser blocked when GUC is off, status=%', status;
    END IF;
    RAISE NOTICE 'TEST 2 PASSED: non-superuser df.start() works when GUC is off';
END $$;
DROP TABLE _su_guc_t2;

-- ============================================================
-- Test 3: Forgery caught by load_function_graph (instance level)
--
-- Tamper submitted_by immediately after df.start(), before the worker has
-- a chance to call load_function_graph.  load_function_graph reads
-- submitted_by and rejects the instance because the GUC is off.
-- ============================================================

DROP TABLE IF EXISTS _su_guc_t3;
CREATE TABLE _su_guc_t3 (instance_id TEXT);
GRANT ALL ON _su_guc_t3 TO df_e2e_user, su_guc_forger;

SET SESSION AUTHORIZATION df_e2e_user;
INSERT INTO _su_guc_t3 SELECT df.start('SELECT 1', 'su-guc-test3-load-guard');
RESET SESSION AUTHORIZATION;

-- Tamper immediately — worker hasn't picked this up yet.
SET SESSION AUTHORIZATION su_guc_forger;
DO $$
DECLARE
    inst TEXT;
BEGIN
    SELECT instance_id INTO inst FROM _su_guc_t3;

    UPDATE df.instances
    SET    submitted_by = 'postgres'::regrole
    WHERE  id = inst;

    UPDATE df.nodes
    SET    submitted_by = 'postgres'::regrole
    WHERE  instance_id = inst;
END $$;
RESET SESSION AUTHORIZATION;

DO $$
DECLARE
    inst_id TEXT;
    status  TEXT;
BEGIN
    SELECT instance_id INTO inst_id FROM _su_guc_t3;
    SELECT df.await_instance(inst_id, 30) INTO status;

    IF lower(status) != 'failed' THEN
        RAISE EXCEPTION 'TEST 3 FAILED: expected failed, got: %', status;
    END IF;

    -- Verify no individual node was marked failed (the rejection happened
    -- before any node execution, at the load_function_graph level).
    IF EXISTS (
        SELECT 1 FROM df.nodes n
        WHERE n.instance_id = inst_id AND n.status = 'failed'
    ) THEN
        RAISE EXCEPTION 'TEST 3 FAILED: a node was marked failed — expected instance-level rejection only';
    END IF;

    RAISE NOTICE 'TEST 3 PASSED: forgery caught by load_function_graph (instance-level rejection)';
END $$;

DROP TABLE _su_guc_t3;

-- ============================================================
-- Test 4: Post-load tampering has no effect (immutable cached graph)
--
-- execute_sql uses the in-memory FunctionGraph cached by duroxide after
-- load_function_graph runs.  That cache is immutable: an attacker who
-- tampers df.nodes after graph load cannot affect execution identity.
-- The test below verifies that a legitimately submitted instance
-- completes successfully even if df.nodes is tampered afterward —
-- confirming the cache is used, not the live table.
-- ============================================================

DROP TABLE IF EXISTS su_guc_executed_sentinel;
CREATE TABLE su_guc_executed_sentinel (marker TEXT);
GRANT ALL ON su_guc_executed_sentinel TO df_e2e_user;

DROP TABLE IF EXISTS _su_guc_t4;
CREATE TABLE _su_guc_t4 (instance_id TEXT);
GRANT ALL ON _su_guc_t4 TO df_e2e_user, su_guc_forger;

SET SESSION AUTHORIZATION df_e2e_user;
INSERT INTO _su_guc_t4
SELECT df.start(
    df.sleep(2) ~> 'INSERT INTO su_guc_executed_sentinel VALUES (''legitimate SQL ran'')',
    'su-guc-test4-cache-immutable'
);
RESET SESSION AUTHORIZATION;

-- Wait for load_function_graph to complete before tampering.
SELECT pg_sleep(1);

-- Tamper after graph is already cached — should have no effect on execution.
SET SESSION AUTHORIZATION su_guc_forger;
DO $$
DECLARE
    inst TEXT;
BEGIN
    SELECT instance_id INTO inst FROM _su_guc_t4;

    UPDATE df.instances
    SET    submitted_by = 'postgres'::regrole
    WHERE  id = inst;

    UPDATE df.nodes
    SET    submitted_by = 'postgres'::regrole
    WHERE  instance_id = inst;
END $$;
RESET SESSION AUTHORIZATION;

DO $$
DECLARE
    inst_id    TEXT;
    status     TEXT;
BEGIN
    SELECT instance_id INTO inst_id FROM _su_guc_t4;
    SELECT df.await_instance(inst_id, 30) INTO status;

    -- The instance must complete: execute_sql ran with the cached (clean)
    -- identity and never saw the tampered superuser in df.nodes.
    IF lower(status) != 'completed' THEN
        RAISE EXCEPTION 'TEST 4 FAILED: expected completed (cache immutable), got: %', status;
    END IF;

    -- The sentinel must contain the row — the legitimate SQL ran.
    IF NOT EXISTS (SELECT 1 FROM su_guc_executed_sentinel) THEN
        RAISE EXCEPTION 'TEST 4 FAILED: sentinel is empty — legitimate SQL did not run';
    END IF;

    RAISE NOTICE 'TEST 4 PASSED: post-load tamper had no effect; graph cache is immutable';
END $$;

DROP TABLE _su_guc_t4;

-- ============================================================
-- Test 5: Cross-iteration tamper has no effect (frozen graph snapshot)
--
-- Loop functions call continue_as_new on each iteration, which discards
-- duroxide history and starts a fresh orchestration generation.  The graph
-- is NOT re-read on those generations: it is loaded once when the instance
-- starts and then carried inline through every child sub-orchestration input
-- and every continue_as_new envelope.
--
-- Attack: tamper submitted_by between iterations.  Because no generation
-- re-reads df.instances or df.nodes, the tampered value is never loaded --
-- the loop keeps running under the identity captured at instance start.
-- The forgery is neutralized by construction rather than detected after the
-- fact, which is strictly stronger: a re-read would make the tamper take
-- effect and then rely on the superuser check to catch it, and that check
-- only rejects escalation *to* a superuser (a redirect to a different
-- non-superuser role would pass).
--
-- Expected: the loop completes normally, runs both iterations, and every
-- statement executes as df_e2e_user -- never as the forged superuser.
-- ============================================================
DROP TABLE IF EXISTS su_guc_loop_sentinel;
CREATE TABLE su_guc_loop_sentinel (iteration INT, executed_by TEXT);
GRANT ALL ON su_guc_loop_sentinel TO df_e2e_user;

DROP TABLE IF EXISTS _su_guc_t5;
CREATE TABLE _su_guc_t5 (instance_id TEXT);
GRANT ALL ON _su_guc_t5 TO df_e2e_user, su_guc_forger;

-- Start a loop that runs through continue_as_new at least once.
-- Each iteration records its number and the role it actually executed as.
-- Iteration 1 records iteration=1 and continues (count < 2); iteration 2
-- records iteration=2 and breaks.  We tamper AFTER iteration 1 completes,
-- so the tamper lands squarely on the continue_as_new boundary.  The
-- df.sleep(2) at the top of the loop body gives the tamper time to land
-- before iteration 2's SQL runs.
SET SESSION AUTHORIZATION df_e2e_user;
INSERT INTO _su_guc_t5
SELECT df.start(
    @> (
        df.sleep(2)
        ~> 'INSERT INTO su_guc_loop_sentinel VALUES (
            (SELECT COALESCE(MAX(iteration), 0) + 1 FROM su_guc_loop_sentinel),
            current_user
        ) RETURNING iteration'
        ~> df.if(
            'SELECT (SELECT COUNT(*) FROM su_guc_loop_sentinel) >= 2',
            df.break(),
            df.sleep(0)
        )
    ),
    'su-guc-test5-loop-tamper'
);
RESET SESSION AUTHORIZATION;

-- Wait for exactly 1 row to appear (iteration 1 done, continue_as_new fired).
DO $$
DECLARE
    attempts INT := 0;
BEGIN
    LOOP
        EXIT WHEN (SELECT COUNT(*) FROM su_guc_loop_sentinel) >= 1 OR attempts > 150;
        PERFORM pg_sleep(0.1);
        attempts := attempts + 1;
    END LOOP;
END $$;

-- Iteration 1 completed.  The loop is about to continue_as_new for
-- iteration 2.  Tamper now -- it must have no observable effect.
SET SESSION AUTHORIZATION su_guc_forger;
DO $$
DECLARE
    inst TEXT;
BEGIN
    SELECT instance_id INTO inst FROM _su_guc_t5;
    UPDATE df.instances SET submitted_by = 'postgres'::regrole WHERE id = inst;
    UPDATE df.nodes      SET submitted_by = 'postgres'::regrole WHERE instance_id = inst;
END $$;
RESET SESSION AUTHORIZATION;

DO $$
DECLARE
    inst_id TEXT;
    status  TEXT;
    iter_count INT;
    forged_count INT;
BEGIN
    SELECT instance_id INTO inst_id FROM _su_guc_t5;
    SELECT df.await_instance(inst_id, 30) INTO status;

    -- The tamper is never read, so the loop runs to normal completion.
    IF lower(status) != 'completed' THEN
        RAISE EXCEPTION 'TEST 5 FAILED: expected completed after cross-iteration tamper, got: %', status;
    END IF;

    -- Both iterations must have run: the tamper must not have stalled the loop.
    SELECT COUNT(*) INTO iter_count FROM su_guc_loop_sentinel;
    IF iter_count != 2 THEN
        RAISE EXCEPTION 'TEST 5 FAILED: expected 2 iteration rows, found %', iter_count;
    END IF;

    -- The critical assertion: no statement ran under the forged superuser
    -- identity.  Every iteration, including the one after the tamper, must
    -- have executed as the role that submitted the instance.
    SELECT COUNT(*) INTO forged_count
      FROM su_guc_loop_sentinel
     WHERE executed_by IS DISTINCT FROM 'df_e2e_user';
    IF forged_count != 0 THEN
        RAISE EXCEPTION 'TEST 5 FAILED: % iteration(s) executed under a forged identity: %',
            forged_count,
            (SELECT string_agg(DISTINCT executed_by, ', ') FROM su_guc_loop_sentinel);
    END IF;

    RAISE NOTICE 'TEST 5 PASSED: cross-iteration tamper neutralized by frozen graph snapshot (iterations=%, all executed as df_e2e_user, status=%)', iter_count, status;
END $$;

DROP TABLE _su_guc_t5;

-- ============================================================
-- Cleanup
-- ============================================================
DROP TABLE IF EXISTS su_guc_loop_sentinel;
DROP TABLE IF EXISTS su_guc_executed_sentinel;

DO $teardown$
BEGIN
    PERFORM pg_terminate_backend(pid)
      FROM pg_stat_activity
      WHERE usename = 'su_guc_forger' AND pid <> pg_backend_pid();

    BEGIN DROP OWNED BY su_guc_forger; EXCEPTION WHEN undefined_object THEN NULL; END;
    BEGIN DROP ROLE su_guc_forger;     EXCEPTION WHEN undefined_object THEN NULL; END;
END $teardown$;

SELECT 'TEST PASSED' AS result;
