-- Copyright (c) Microsoft Corporation. -- Licensed under the PostgreSQL License. -- Merged from: 26_superuser_scenarios, 27_user_isolation -- Tests: superuser durable SQL (pg_authid access), per-user table isolation, -- NOLOGIN group role rejection, SECURITY DEFINER semantics, dropped role handling -- Runs as postgres throughout (creates/drops roles and user-owned tables) -- === Test: 26_superuser_scenarios === CREATE TEMP TABLE _test_state (instance_id TEXT); INSERT INTO _test_state SELECT df.start( df.sql('SELECT (SELECT rolname FROM pg_authid LIMIT 1) AS any_role'), 'test-superuser-pg_authid' ); DO $$ DECLARE inst_id TEXT; status TEXT; result TEXT; BEGIN SELECT instance_id INTO inst_id FROM _test_state; SELECT df.await_instance(inst_id) INTO status; IF status != 'completed' THEN RAISE EXCEPTION 'TEST FAILED: expected completed, got %', status; END IF; SELECT r INTO result FROM df.result(inst_id) r; IF result NOT LIKE '%any_role%' THEN RAISE EXCEPTION 'TEST FAILED: expected result to contain any_role, got %', result; END IF; RAISE NOTICE 'TEST PASSED: superuser durable sql (pg_authid)'; END $$; DROP TABLE _test_state; -- === Test: 27_user_isolation === -- Setup: Create two test users with separate tables DO $setup$ BEGIN PERFORM pg_terminate_backend(pid) FROM pg_stat_activity WHERE usename IN ('iso_alice', 'iso_bob') AND pid <> pg_backend_pid(); BEGIN DROP OWNED BY iso_alice; EXCEPTION WHEN undefined_object THEN NULL; END; BEGIN DROP OWNED BY iso_bob; EXCEPTION WHEN undefined_object THEN NULL; END; BEGIN DROP ROLE iso_alice; EXCEPTION WHEN undefined_object THEN NULL; END; BEGIN DROP ROLE iso_bob; EXCEPTION WHEN undefined_object THEN NULL; END; END $setup$; CREATE ROLE iso_alice LOGIN; CREATE ROLE iso_bob LOGIN; SELECT df.grant_usage('iso_alice'); SELECT df.grant_usage('iso_bob'); GRANT TEMPORARY ON DATABASE postgres TO iso_alice, iso_bob; CREATE TABLE IF NOT EXISTS iso_alice_data (id SERIAL PRIMARY KEY, value TEXT); ALTER TABLE iso_alice_data OWNER TO iso_alice; INSERT INTO iso_alice_data (value) VALUES ('alice secret') ON CONFLICT DO NOTHING; CREATE TABLE IF NOT EXISTS iso_bob_data (id SERIAL PRIMARY KEY, value TEXT); ALTER TABLE iso_bob_data OWNER TO iso_bob; INSERT INTO iso_bob_data (value) VALUES ('bob secret') ON CONFLICT DO NOTHING; -- Test 1: Alice can access her own table via durable function SET SESSION AUTHORIZATION iso_alice; CREATE TEMP TABLE _test_state_1 (instance_id TEXT); INSERT INTO _test_state_1 SELECT df.start(df.sql('SELECT value FROM iso_alice_data LIMIT 1'), 'iso-alice-own'); RESET SESSION AUTHORIZATION; DO $$ DECLARE inst_id TEXT; final_status TEXT; result TEXT; BEGIN SELECT instance_id INTO inst_id FROM _test_state_1; SELECT df.await_instance(inst_id, 30) INTO final_status; IF final_status != 'completed' THEN RAISE EXCEPTION 'TEST FAILED (Test 1 - alice own table): expected completed, got %', final_status; END IF; SELECT r INTO result FROM df.result(inst_id) r; IF result IS NULL OR result NOT LIKE '%alice secret%' THEN RAISE EXCEPTION 'TEST FAILED (Test 1): expected alice secret, got %', result; END IF; RAISE NOTICE 'Test 1 PASSED: Alice can access her own table'; END $$; DROP TABLE _test_state_1; -- Test 2: Alice CANNOT access Bob's table via durable function SET SESSION AUTHORIZATION iso_alice; CREATE TEMP TABLE _test_state_2 (instance_id TEXT); INSERT INTO _test_state_2 SELECT df.start(df.sql('SELECT value FROM iso_bob_data LIMIT 1'), 'iso-alice-bob'); RESET SESSION AUTHORIZATION; DO $$ DECLARE inst_id TEXT; final_status TEXT; BEGIN SELECT instance_id INTO inst_id FROM _test_state_2; SELECT df.await_instance(inst_id, 30) INTO final_status; IF final_status != 'failed' THEN RAISE EXCEPTION 'TEST FAILED (Test 2 - alice access bob table): expected failed, got %', final_status; END IF; RAISE NOTICE 'Test 2 PASSED: Alice cannot access Bob''s table'; END $$; DROP TABLE _test_state_2; -- Test 3: Bob can access his own table via durable function SET SESSION AUTHORIZATION iso_bob; CREATE TEMP TABLE _test_state_3 (instance_id TEXT); INSERT INTO _test_state_3 SELECT df.start(df.sql('SELECT value FROM iso_bob_data LIMIT 1'), 'iso-bob-own'); RESET SESSION AUTHORIZATION; DO $$ DECLARE inst_id TEXT; final_status TEXT; result TEXT; BEGIN SELECT instance_id INTO inst_id FROM _test_state_3; SELECT df.await_instance(inst_id, 30) INTO final_status; IF final_status != 'completed' THEN RAISE EXCEPTION 'TEST FAILED (Test 3 - bob own table): expected completed, got %', final_status; END IF; SELECT r INTO result FROM df.result(inst_id) r; IF result IS NULL OR result NOT LIKE '%bob secret%' THEN RAISE EXCEPTION 'TEST FAILED (Test 3): expected bob secret, got %', result; END IF; RAISE NOTICE 'Test 3 PASSED: Bob can access his own table'; END $$; DROP TABLE _test_state_3; -- Test 4: Bob CANNOT access Alice's table via durable function SET SESSION AUTHORIZATION iso_bob; CREATE TEMP TABLE _test_state_4 (instance_id TEXT); INSERT INTO _test_state_4 SELECT df.start(df.sql('SELECT value FROM iso_alice_data LIMIT 1'), 'iso-bob-alice'); RESET SESSION AUTHORIZATION; DO $$ DECLARE inst_id TEXT; final_status TEXT; BEGIN SELECT instance_id INTO inst_id FROM _test_state_4; SELECT df.await_instance(inst_id, 30) INTO final_status; IF final_status != 'failed' THEN RAISE EXCEPTION 'TEST FAILED (Test 4 - bob access alice table): expected failed, got %', final_status; END IF; RAISE NOTICE 'Test 4 PASSED: Bob cannot access Alice''s table'; END $$; DROP TABLE _test_state_4; -- Test 5: SET ROLE with a NOLOGIN group role → df.start() rejects DO $group_setup$ BEGIN BEGIN DROP OWNED BY iso_analysts; EXCEPTION WHEN undefined_object THEN NULL; END; BEGIN DROP ROLE iso_analysts; EXCEPTION WHEN undefined_object THEN NULL; END; END $group_setup$; CREATE ROLE iso_analysts NOLOGIN; CREATE TABLE IF NOT EXISTS iso_analyst_data (id SERIAL PRIMARY KEY, value TEXT); ALTER TABLE iso_analyst_data OWNER TO iso_analysts; INSERT INTO iso_analyst_data (value) VALUES ('analyst report') ON CONFLICT DO NOTHING; GRANT iso_analysts TO iso_alice; SELECT df.grant_usage('iso_analysts'); GRANT TEMPORARY ON DATABASE postgres TO iso_analysts; SET SESSION AUTHORIZATION iso_alice; SET ROLE iso_analysts; DO $$ BEGIN PERFORM df.start(df.sql('SELECT value FROM iso_analyst_data LIMIT 1'), 'iso-set-role-nologin'); RAISE EXCEPTION 'TEST FAILED (Test 5a): df.start() should have rejected NOLOGIN role'; EXCEPTION WHEN OTHERS THEN IF SQLERRM LIKE '%LOGIN%' THEN RAISE NOTICE 'Test 5a PASSED: df.start() rejects NOLOGIN group role with LOGIN error'; ELSE RAISE EXCEPTION 'TEST FAILED (Test 5a): unexpected error: %', SQLERRM; END IF; END $$; RESET ROLE; RESET SESSION AUTHORIZATION; -- Test 5b: Grant LOGIN to group role → df.start() should succeed ALTER ROLE iso_analysts LOGIN; SET SESSION AUTHORIZATION iso_alice; SET ROLE iso_analysts; CREATE TEMP TABLE _test_state_5b (instance_id TEXT); INSERT INTO _test_state_5b SELECT df.start(df.sql('SELECT value FROM iso_analyst_data LIMIT 1'), 'iso-set-role-login'); RESET ROLE; RESET SESSION AUTHORIZATION; DO $$ DECLARE inst_id TEXT; final_status TEXT; result TEXT; inst_submitted TEXT; BEGIN SELECT instance_id INTO inst_id FROM _test_state_5b; SELECT df.await_instance(inst_id, 30) INTO final_status; IF final_status != 'completed' THEN RAISE EXCEPTION 'TEST FAILED (Test 5b - SET ROLE with LOGIN): expected completed, got %', final_status; END IF; SELECT r INTO result FROM df.result(inst_id) r; IF result IS NULL OR result NOT LIKE '%analyst report%' THEN RAISE EXCEPTION 'TEST FAILED (Test 5b): expected analyst report, got %', result; END IF; SELECT submitted_by::text INTO inst_submitted FROM df.instances WHERE id = inst_id; IF inst_submitted != 'iso_analysts' THEN RAISE EXCEPTION 'TEST FAILED (Test 5b): expected submitted_by=iso_analysts, got %', inst_submitted; END IF; RAISE NOTICE 'Test 5b PASSED: SET ROLE with LOGIN group role works (submitted_by=iso_analysts)'; END $$; DROP TABLE _test_state_5b; ALTER ROLE iso_analysts NOLOGIN; -- Test 6: SECURITY DEFINER function - captures definer identity CREATE TABLE IF NOT EXISTS iso_superuser_secrets (id SERIAL PRIMARY KEY, value TEXT); INSERT INTO iso_superuser_secrets (value) VALUES ('classified') ON CONFLICT DO NOTHING; CREATE OR REPLACE FUNCTION iso_submit_as_definer(q TEXT) RETURNS TEXT LANGUAGE SQL SECURITY DEFINER AS $$ SELECT df.start(df.sql(q), 'secdef-test'); $$; GRANT EXECUTE ON FUNCTION iso_submit_as_definer TO iso_alice; -- Test 6a: Alice calls SECURITY DEFINER to query her own table SET SESSION AUTHORIZATION iso_alice; CREATE TEMP TABLE _test_state_6a (instance_id TEXT); INSERT INTO _test_state_6a SELECT iso_submit_as_definer('SELECT value FROM iso_alice_data LIMIT 1'); RESET SESSION AUTHORIZATION; DO $$ DECLARE inst_id TEXT; final_status TEXT; result TEXT; BEGIN SELECT instance_id INTO inst_id FROM _test_state_6a; SELECT df.await_instance(inst_id, 30) INTO final_status; IF final_status != 'completed' THEN RAISE EXCEPTION 'TEST FAILED (Test 6a - SECURITY DEFINER access alice table): expected completed, got %', final_status; END IF; SELECT r INTO result FROM df.result(inst_id) r; IF result IS NULL OR result NOT LIKE '%alice secret%' THEN RAISE EXCEPTION 'TEST FAILED (Test 6a): expected alice secret, got %', result; END IF; RAISE NOTICE 'Test 6a PASSED: SECURITY DEFINER runs as definer, can access alice table'; END $$; DROP TABLE _test_state_6a; -- Test 6b: Alice calls SECURITY DEFINER to query superuser-only table SET SESSION AUTHORIZATION iso_alice; CREATE TEMP TABLE _test_state_6b (instance_id TEXT); INSERT INTO _test_state_6b SELECT iso_submit_as_definer('SELECT value FROM iso_superuser_secrets LIMIT 1'); RESET SESSION AUTHORIZATION; DO $$ DECLARE inst_id TEXT; final_status TEXT; result TEXT; BEGIN SELECT instance_id INTO inst_id FROM _test_state_6b; SELECT df.await_instance(inst_id, 30) INTO final_status; IF final_status != 'completed' THEN RAISE EXCEPTION 'TEST FAILED (Test 6b - SECURITY DEFINER access superuser table): expected completed (runs as definer), got %', final_status; END IF; SELECT r INTO result FROM df.result(inst_id) r; IF result IS NULL OR result NOT LIKE '%classified%' THEN RAISE EXCEPTION 'TEST FAILED (Test 6b): expected classified, got %', result; END IF; RAISE NOTICE 'Test 6b PASSED: SECURITY DEFINER runs as definer, CAN access superuser table (expected in simplified model)'; END $$; DROP TABLE _test_state_6b; DROP FUNCTION IF EXISTS iso_submit_as_definer(TEXT); DROP TABLE IF EXISTS iso_superuser_secrets CASCADE; -- Test 7: Dropped role during execution DO $test7_setup$ BEGIN BEGIN DROP OWNED BY iso_ephemeral; EXCEPTION WHEN undefined_object THEN NULL; END; BEGIN DROP ROLE iso_ephemeral; EXCEPTION WHEN undefined_object THEN NULL; END; END $test7_setup$; CREATE ROLE iso_ephemeral LOGIN; SELECT df.grant_usage('iso_ephemeral'); GRANT TEMPORARY ON DATABASE postgres TO iso_ephemeral; DROP TABLE IF EXISTS _test_state_7_persistent; CREATE TABLE _test_state_7_persistent (instance_id TEXT); GRANT INSERT ON _test_state_7_persistent TO iso_ephemeral; SET SESSION AUTHORIZATION iso_ephemeral; INSERT INTO _test_state_7_persistent SELECT df.start(df.sleep(3) ~> df.sql('SELECT 1'), 'ephemeral-test'); RESET SESSION AUTHORIZATION; DO $$ DECLARE inst_id TEXT; attempts INT := 0; node_count INT; BEGIN SELECT instance_id INTO inst_id FROM _test_state_7_persistent; LOOP SELECT COUNT(*) INTO node_count FROM df.nodes WHERE instance_id = inst_id AND status != 'pending' AND status IS NOT NULL; EXIT WHEN node_count > 0 OR attempts > 100; PERFORM pg_sleep(0.1); attempts := attempts + 1; END LOOP; IF node_count = 0 THEN RAISE EXCEPTION 'TEST SETUP FAILED (Test 7): sleep node never started'; END IF; RAISE NOTICE 'Test 7: Sleep node started, now dropping role iso_ephemeral'; DROP OWNED BY iso_ephemeral; DROP ROLE iso_ephemeral; RAISE NOTICE 'Test 7: Dropped role iso_ephemeral, SQL node should fail when it tries to execute'; END $$; DO $$ DECLARE inst_id TEXT; final_status TEXT; attempts INT := 0; BEGIN SELECT instance_id INTO inst_id FROM _test_state_7_persistent; LOOP SELECT status INTO final_status FROM df.instances WHERE id = inst_id; EXIT WHEN lower(final_status) IN ('failed', 'completed') OR attempts > 100; PERFORM pg_sleep(0.1); attempts := attempts + 1; END LOOP; IF final_status IS NULL THEN RAISE EXCEPTION 'TEST FAILED (Test 7 - dropped role): instance not found'; END IF; IF lower(final_status) != 'failed' THEN RAISE EXCEPTION 'TEST FAILED (Test 7 - dropped role): expected failed, got %', final_status; END IF; RAISE NOTICE 'Test 7 PASSED: Dropped role causes clear failure (status=failed)'; END $$; DROP TABLE _test_state_7_persistent; -- Cleanup DROP TABLE IF EXISTS iso_alice_data CASCADE; DROP TABLE IF EXISTS iso_bob_data CASCADE; DROP TABLE IF EXISTS iso_analyst_data CASCADE; REVOKE iso_analysts FROM iso_alice; DO $cleanup$ BEGIN BEGIN DROP OWNED BY iso_analysts; EXCEPTION WHEN undefined_object THEN NULL; END; BEGIN DROP OWNED BY iso_alice; EXCEPTION WHEN undefined_object THEN NULL; END; BEGIN DROP OWNED BY iso_bob; EXCEPTION WHEN undefined_object THEN NULL; END; BEGIN DROP ROLE iso_analysts; EXCEPTION WHEN undefined_object THEN NULL; END; BEGIN DROP ROLE iso_alice; EXCEPTION WHEN undefined_object THEN NULL; END; BEGIN DROP ROLE iso_bob; EXCEPTION WHEN undefined_object THEN NULL; END; END $cleanup$; SELECT 'TEST PASSED' AS result;