diff --git a/packages/core/src/init/client.ts b/packages/core/src/init/client.ts index ca7a7fa91..67bad480d 100644 --- a/packages/core/src/init/client.ts +++ b/packages/core/src/init/client.ts @@ -69,42 +69,52 @@ DECLARE v_username TEXT := '${username.replace(/'/g, "''")}'; v_password TEXT := '${password.replace(/'/g, "''")}'; BEGIN - BEGIN - EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', v_username, v_password); - EXCEPTION - WHEN duplicate_object THEN - -- Role already exists; optionally sync attributes here with ALTER ROLE - NULL; - END; + IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = v_username) THEN + BEGIN + EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', v_username, v_password); + EXCEPTION + WHEN duplicate_object OR unique_violation THEN + NULL; + END; + END IF; END $do$; --- Robust GRANTs under concurrency: GRANT can race on pg_auth_members unique index. --- Catch unique_violation (23505) and continue so CI/CD concurrent jobs don't fail. DO $do$ DECLARE v_username TEXT := '${username.replace(/'/g, "''")}'; BEGIN - BEGIN - EXECUTE format('GRANT %I TO %I', 'anonymous', v_username); - EXCEPTION - WHEN unique_violation THEN - -- Membership was granted concurrently; ignore. - NULL; - WHEN undefined_object THEN - -- One of the roles doesn't exist yet; order operations as needed. - RAISE NOTICE 'Missing role when granting % to %', 'anonymous', v_username; - END; + IF NOT EXISTS ( + SELECT 1 FROM pg_auth_members am + JOIN pg_roles r1 ON am.roleid = r1.oid + JOIN pg_roles r2 ON am.member = r2.oid + WHERE r1.rolname = 'anonymous' AND r2.rolname = v_username + ) THEN + BEGIN + EXECUTE format('GRANT %I TO %I', 'anonymous', v_username); + EXCEPTION + WHEN unique_violation THEN + NULL; + WHEN undefined_object THEN + RAISE NOTICE 'Missing role when granting % to %', 'anonymous', v_username; + END; + END IF; - BEGIN - EXECUTE format('GRANT %I TO %I', 'authenticated', v_username); - EXCEPTION - WHEN unique_violation THEN - -- Membership was granted concurrently; ignore. - NULL; - WHEN undefined_object THEN - RAISE NOTICE 'Missing role when granting % to %', 'authenticated', v_username; - END; + IF NOT EXISTS ( + SELECT 1 FROM pg_auth_members am + JOIN pg_roles r1 ON am.roleid = r1.oid + JOIN pg_roles r2 ON am.member = r2.oid + WHERE r1.rolname = 'authenticated' AND r2.rolname = v_username + ) THEN + BEGIN + EXECUTE format('GRANT %I TO %I', 'authenticated', v_username); + EXCEPTION + WHEN unique_violation THEN + NULL; + WHEN undefined_object THEN + RAISE NOTICE 'Missing role when granting % to %', 'authenticated', v_username; + END; + END IF; END $do$; COMMIT; diff --git a/packages/core/src/init/sql/bootstrap-roles.sql b/packages/core/src/init/sql/bootstrap-roles.sql index 6b455b741..bd70a068a 100644 --- a/packages/core/src/init/sql/bootstrap-roles.sql +++ b/packages/core/src/init/sql/bootstrap-roles.sql @@ -1,32 +1,32 @@ BEGIN; DO $do$ BEGIN - -- anonymous - BEGIN - EXECUTE format('CREATE ROLE %I', 'anonymous'); - EXCEPTION - WHEN duplicate_object THEN - -- Role already exists; optionally sync attributes here with ALTER ROLE - NULL; - END; + IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = 'anonymous') THEN + BEGIN + EXECUTE format('CREATE ROLE %I', 'anonymous'); + EXCEPTION + WHEN duplicate_object OR unique_violation THEN + NULL; + END; + END IF; - -- authenticated - BEGIN - EXECUTE format('CREATE ROLE %I', 'authenticated'); - EXCEPTION - WHEN duplicate_object THEN - -- Role already exists; optionally sync attributes here with ALTER ROLE - NULL; - END; + IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = 'authenticated') THEN + BEGIN + EXECUTE format('CREATE ROLE %I', 'authenticated'); + EXCEPTION + WHEN duplicate_object OR unique_violation THEN + NULL; + END; + END IF; - -- administrator - BEGIN - EXECUTE format('CREATE ROLE %I', 'administrator'); - EXCEPTION - WHEN duplicate_object THEN - -- Role already exists; optionally sync attributes here with ALTER ROLE - NULL; - END; + IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = 'administrator') THEN + BEGIN + EXECUTE format('CREATE ROLE %I', 'administrator'); + EXCEPTION + WHEN duplicate_object OR unique_violation THEN + NULL; + END; + END IF; END $do$; @@ -52,4 +52,4 @@ ALTER USER administrator WITH NOLOGIN; ALTER USER administrator WITH NOREPLICATION; -- they CAN bypass RLS ALTER USER administrator WITH BYPASSRLS; -COMMIT; \ No newline at end of file +COMMIT; diff --git a/packages/core/src/init/sql/bootstrap-test-roles.sql b/packages/core/src/init/sql/bootstrap-test-roles.sql index d5bc68f21..3110483e9 100644 --- a/packages/core/src/init/sql/bootstrap-test-roles.sql +++ b/packages/core/src/init/sql/bootstrap-test-roles.sql @@ -1,72 +1,107 @@ BEGIN; DO $do$ BEGIN - BEGIN - EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', 'app_user', 'app_password'); - EXCEPTION - WHEN duplicate_object THEN - -- Role already exists; optionally sync attributes here with ALTER ROLE - NULL; - END; + IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = 'app_user') THEN + BEGIN + EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', 'app_user', 'app_password'); + EXCEPTION + WHEN duplicate_object OR unique_violation THEN + NULL; + END; + END IF; - BEGIN - EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', 'app_admin', 'admin_password'); - EXCEPTION - WHEN duplicate_object THEN - -- Role already exists; optionally sync attributes here with ALTER ROLE - NULL; - END; + IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = 'app_admin') THEN + BEGIN + EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', 'app_admin', 'admin_password'); + EXCEPTION + WHEN duplicate_object OR unique_violation THEN + NULL; + END; + END IF; END $do$; DO $do$ BEGIN - BEGIN - EXECUTE format('GRANT %I TO %I', 'anonymous', 'app_user'); - EXCEPTION - WHEN unique_violation THEN - -- Membership was granted concurrently; ignore. - NULL; - WHEN undefined_object THEN - -- One of the roles doesn't exist yet; order operations as needed. - RAISE NOTICE 'Missing role when granting % to %', 'anonymous', 'app_user'; - END; + IF NOT EXISTS ( + SELECT 1 FROM pg_auth_members am + JOIN pg_roles r1 ON am.roleid = r1.oid + JOIN pg_roles r2 ON am.member = r2.oid + WHERE r1.rolname = 'anonymous' AND r2.rolname = 'app_user' + ) THEN + BEGIN + EXECUTE format('GRANT %I TO %I', 'anonymous', 'app_user'); + EXCEPTION + WHEN unique_violation THEN + NULL; + WHEN undefined_object THEN + RAISE NOTICE 'Missing role when granting % to %', 'anonymous', 'app_user'; + END; + END IF; - BEGIN - EXECUTE format('GRANT %I TO %I', 'authenticated', 'app_user'); - EXCEPTION - WHEN unique_violation THEN - NULL; - WHEN undefined_object THEN - RAISE NOTICE 'Missing role when granting % to %', 'authenticated', 'app_user'; - END; + IF NOT EXISTS ( + SELECT 1 FROM pg_auth_members am + JOIN pg_roles r1 ON am.roleid = r1.oid + JOIN pg_roles r2 ON am.member = r2.oid + WHERE r1.rolname = 'authenticated' AND r2.rolname = 'app_user' + ) THEN + BEGIN + EXECUTE format('GRANT %I TO %I', 'authenticated', 'app_user'); + EXCEPTION + WHEN unique_violation THEN + NULL; + WHEN undefined_object THEN + RAISE NOTICE 'Missing role when granting % to %', 'authenticated', 'app_user'; + END; + END IF; - BEGIN - EXECUTE format('GRANT %I TO %I', 'anonymous', 'administrator'); - EXCEPTION - WHEN unique_violation THEN - NULL; - WHEN undefined_object THEN - RAISE NOTICE 'Missing role when granting % to %', 'anonymous', 'administrator'; - END; + IF NOT EXISTS ( + SELECT 1 FROM pg_auth_members am + JOIN pg_roles r1 ON am.roleid = r1.oid + JOIN pg_roles r2 ON am.member = r2.oid + WHERE r1.rolname = 'anonymous' AND r2.rolname = 'administrator' + ) THEN + BEGIN + EXECUTE format('GRANT %I TO %I', 'anonymous', 'administrator'); + EXCEPTION + WHEN unique_violation THEN + NULL; + WHEN undefined_object THEN + RAISE NOTICE 'Missing role when granting % to %', 'anonymous', 'administrator'; + END; + END IF; - BEGIN - EXECUTE format('GRANT %I TO %I', 'authenticated', 'administrator'); - EXCEPTION - WHEN unique_violation THEN - NULL; - WHEN undefined_object THEN - RAISE NOTICE 'Missing role when granting % to %', 'authenticated', 'administrator'; - END; + IF NOT EXISTS ( + SELECT 1 FROM pg_auth_members am + JOIN pg_roles r1 ON am.roleid = r1.oid + JOIN pg_roles r2 ON am.member = r2.oid + WHERE r1.rolname = 'authenticated' AND r2.rolname = 'administrator' + ) THEN + BEGIN + EXECUTE format('GRANT %I TO %I', 'authenticated', 'administrator'); + EXCEPTION + WHEN unique_violation THEN + NULL; + WHEN undefined_object THEN + RAISE NOTICE 'Missing role when granting % to %', 'authenticated', 'administrator'; + END; + END IF; - BEGIN - EXECUTE format('GRANT %I TO %I', 'administrator', 'app_admin'); - EXCEPTION - WHEN unique_violation THEN - NULL; - WHEN undefined_object THEN - RAISE NOTICE 'Missing role when granting % to %', 'administrator', 'app_admin'; - END; + IF NOT EXISTS ( + SELECT 1 FROM pg_auth_members am + JOIN pg_roles r1 ON am.roleid = r1.oid + JOIN pg_roles r2 ON am.member = r2.oid + WHERE r1.rolname = 'administrator' AND r2.rolname = 'app_admin' + ) THEN + BEGIN + EXECUTE format('GRANT %I TO %I', 'administrator', 'app_admin'); + EXCEPTION + WHEN unique_violation THEN + NULL; + WHEN undefined_object THEN + RAISE NOTICE 'Missing role when granting % to %', 'administrator', 'app_admin'; + END; + END IF; END $do$; COMMIT; diff --git a/packages/pgsql-test/src/admin.ts b/packages/pgsql-test/src/admin.ts index 16adf666e..cb015b048 100644 --- a/packages/pgsql-test/src/admin.ts +++ b/packages/pgsql-test/src/admin.ts @@ -155,14 +155,14 @@ $$; v_user TEXT := '${user.replace(/'/g, "''")}'; v_password TEXT := '${password.replace(/'/g, "''")}'; BEGIN - -- Create role if it doesn't exist - BEGIN - EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', v_user, v_password); - EXCEPTION - WHEN duplicate_object THEN - -- Role already exists; optionally sync attributes here with ALTER ROLE - NULL; - END; + IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = v_user) THEN + BEGIN + EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', v_user, v_password); + EXCEPTION + WHEN duplicate_object OR unique_violation THEN + NULL; + END; + END IF; -- CI/CD concurrency note: GRANT role membership can race on pg_auth_members unique index -- We pre-check membership and still catch unique_violation to handle TOCTOU safely.