From 86539275eab69d73e033c8a6cdcdec14d5b12926 Mon Sep 17 00:00:00 2001 From: Brian Date: Thu, 12 Nov 2026 16:14:41 +0900 Subject: [PATCH 1/3] create supabase_admin_monitor role for monitoring services --- .../files/postgresql_config/supautils.conf.j2 | 4 +- .../00000000000000-initial-schema.sql | 13 +++ migrations/tests/database/privs.sql | 7 ++ nix/tests/expected/roles.out | 10 +- nix/tests/expected/z_15_roles.out | 4 +- .../expected/z_15_supabase_monitor_admin.out | 100 ++++++++++++++++ nix/tests/expected/z_17_roles.out | 8 +- .../expected/z_17_supabase_monitor_admin.out | 110 ++++++++++++++++++ nix/tests/expected/z_multigres-17_roles.out | 8 +- .../z_multigres-orioledb-17_roles.out | 10 +- nix/tests/sql/z_15_supabase_monitor_admin.sql | 61 ++++++++++ nix/tests/sql/z_17_supabase_monitor_admin.sql | 65 +++++++++++ testinfra/test_ami_nix.py | 39 +++++++ 13 files changed, 424 insertions(+), 15 deletions(-) create mode 100644 nix/tests/expected/z_15_supabase_monitor_admin.out create mode 100644 nix/tests/expected/z_17_supabase_monitor_admin.out create mode 100644 nix/tests/sql/z_15_supabase_monitor_admin.sql create mode 100644 nix/tests/sql/z_17_supabase_monitor_admin.sql diff --git a/ansible/files/postgresql_config/supautils.conf.j2 b/ansible/files/postgresql_config/supautils.conf.j2 index 63c10761b5..0710677c57 100644 --- a/ansible/files/postgresql_config/supautils.conf.j2 +++ b/ansible/files/postgresql_config/supautils.conf.j2 @@ -13,6 +13,6 @@ supautils.restrict_extension_versions = 'warn' supautils.privileged_extensions_superuser = 'supabase_admin' supautils.privileged_role = 'supabase_privileged_role' supautils.privileged_role_allowed_configs = 'auto_explain.*, deadlock_timeout, log_duration, log_lock_waits, log_min_duration_statement, log_min_error_statement, log_min_messages, log_parameter_max_length, log_replication_commands, log_statement, log_temp_files, pg_net.batch_size, pg_net.ttl, pg_stat_statements.*, pgaudit.log, pgaudit.log_catalog, pgaudit.log_client, pgaudit.log_level, pgaudit.log_relation, pgaudit.log_rows, pgaudit.log_statement, pgaudit.log_statement_once, pgaudit.role, pgrst.*, plan_filter.*, safeupdate.enabled, session_replication_role, track_functions, track_io_timing, wal_compression' -supautils.reserved_memberships = 'pg_read_server_files, pg_write_server_files, pg_execute_server_program, supabase_admin, supabase_auth_admin, supabase_storage_admin, supabase_read_only_user, supabase_realtime_admin, supabase_replication_admin, supabase_etl_admin, dashboard_user, pgbouncer, authenticator' -supautils.reserved_roles = 'supabase_admin, supabase_auth_admin, supabase_storage_admin, supabase_read_only_user, supabase_realtime_admin, supabase_replication_admin, supabase_etl_admin, dashboard_user, pgbouncer, service_role*, authenticator*, authenticated*, anon*, supabase_privileged_role' +supautils.reserved_memberships = 'pg_read_server_files, pg_write_server_files, pg_execute_server_program, supabase_admin, supabase_auth_admin, supabase_storage_admin, supabase_read_only_user, supabase_realtime_admin, supabase_replication_admin, supabase_etl_admin, supabase_monitor_admin, dashboard_user, pgbouncer, authenticator' +supautils.reserved_roles = 'supabase_admin, supabase_auth_admin, supabase_storage_admin, supabase_read_only_user, supabase_realtime_admin, supabase_replication_admin, supabase_etl_admin, supabase_monitor_admin, dashboard_user, pgbouncer, service_role*, authenticator*, authenticated*, anon*, supabase_privileged_role' supautils.hint_roles = 'anon, authenticated, service_role' diff --git a/migrations/db/init-scripts/00000000000000-initial-schema.sql b/migrations/db/init-scripts/00000000000000-initial-schema.sql index b32db6b939..25d22ee610 100644 --- a/migrations/db/init-scripts/00000000000000-initial-schema.sql +++ b/migrations/db/init-scripts/00000000000000-initial-schema.sql @@ -19,6 +19,19 @@ grant create on database postgres to supabase_etl_admin; create role supabase_read_only_user with login bypassrls; grant pg_read_all_data to supabase_read_only_user; +-- Supabase monitoring user: no password and can only connect via localhost, +-- reads every table +-- bypassing RLS, read-only, owns nothing. +-- so the name can never be pre-empted: a plain CREATE fails loudly if it exists. +create role supabase_monitor_admin with login bypassrls connection limit 2; +grant pg_read_all_data, pg_monitor to supabase_monitor_admin; +alter role supabase_monitor_admin set default_transaction_read_only = on; +alter role supabase_monitor_admin set transaction_read_only = on; +alter role supabase_monitor_admin set search_path = ''; +alter role supabase_monitor_admin set statement_timeout = '3s'; +alter role supabase_monitor_admin set lock_timeout = '3s'; +alter role supabase_monitor_admin set idle_in_transaction_session_timeout = '3s'; + -- Extension namespacing create schema if not exists extensions; create extension if not exists "uuid-ossp" with schema extensions; diff --git a/migrations/tests/database/privs.sql b/migrations/tests/database/privs.sql index ea4f1318ab..e7d44ce5ee 100644 --- a/migrations/tests/database/privs.sql +++ b/migrations/tests/database/privs.sql @@ -27,3 +27,10 @@ SELECT schema_privs_are('extensions', 'service_role', array['USAGE']); -- Role memberships SELECT is_member_of('pg_read_all_data', 'postgres'); SELECT is_member_of('pg_signal_backend', 'postgres'); + +-- read-only monitoring role. +SELECT isnt_superuser('supabase_monitor_admin'); +SELECT is_member_of('pg_read_all_data', 'supabase_monitor_admin'); +SELECT is_member_of('pg_monitor', 'supabase_monitor_admin'); +SELECT database_privs_are(current_database(), 'supabase_monitor_admin', ARRAY['CONNECT', 'TEMPORARY']); +SELECT schema_privs_are('public', 'supabase_monitor_admin', array['USAGE']); diff --git a/nix/tests/expected/roles.out b/nix/tests/expected/roles.out index 78a60dc24d..4bd75cbc4d 100644 --- a/nix/tests/expected/roles.out +++ b/nix/tests/expected/roles.out @@ -48,11 +48,12 @@ order by rolname; supabase_auth_admin | t | t | f | f | f | f | -1 | f | supabase_etl_admin | f | t | f | t | f | t | -1 | t | supabase_functions_admin | t | t | f | f | f | f | -1 | f | + supabase_monitor_admin | f | t | f | t | f | f | 2 | t | supabase_privileged_role | f | f | f | t | f | f | -1 | f | supabase_read_only_user | f | t | f | t | f | f | -1 | t | supabase_replication_admin | f | t | f | t | f | t | -1 | f | supabase_storage_admin | t | t | f | f | f | f | -1 | f | -(31 rows) +(32 rows) select rolname, @@ -60,8 +61,8 @@ select from pg_roles r where rolname not in ('pg_create_subscription', 'pg_maintain', 'pg_use_reserved_connections') order by rolname; - rolname | rolconfig -----------------------------+------------------------------------------------------------------------------------------ + rolname | rolconfig +----------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------ anon | {statement_timeout=3s} authenticated | {statement_timeout=8s} authenticator | {"session_preload_libraries=supautils, safeupdate",statement_timeout=8s,lock_timeout=8s} @@ -89,11 +90,12 @@ order by rolname; supabase_auth_admin | {search_path=auth,idle_in_transaction_session_timeout=60000,log_statement=none} supabase_etl_admin | supabase_functions_admin | + supabase_monitor_admin | {default_transaction_read_only=on,transaction_read_only=on,"search_path=\"\"",statement_timeout=3s,lock_timeout=3s,idle_in_transaction_session_timeout=3s} supabase_privileged_role | supabase_read_only_user | {default_transaction_read_only=on} supabase_replication_admin | supabase_storage_admin | {search_path=storage,log_statement=none} -(31 rows) +(32 rows) -- Check all privileges of the roles on the schemas select schema_name, privilege_type, grantee, default_for diff --git a/nix/tests/expected/z_15_roles.out b/nix/tests/expected/z_15_roles.out index c7aaabf361..216b50a787 100644 --- a/nix/tests/expected/z_15_roles.out +++ b/nix/tests/expected/z_15_roles.out @@ -33,10 +33,12 @@ order by supabase_etl_admin | pg_monitor | f supabase_etl_admin | pg_read_all_data | f supabase_etl_admin | supabase_privileged_role | f + supabase_monitor_admin | pg_monitor | f + supabase_monitor_admin | pg_read_all_data | f supabase_read_only_user | pg_monitor | f supabase_read_only_user | pg_read_all_data | f supabase_storage_admin | authenticator | f -(23 rows) +(25 rows) -- Check all privileges of non-superuser roles on functions select diff --git a/nix/tests/expected/z_15_supabase_monitor_admin.out b/nix/tests/expected/z_15_supabase_monitor_admin.out new file mode 100644 index 0000000000..4224791350 --- /dev/null +++ b/nix/tests/expected/z_15_supabase_monitor_admin.out @@ -0,0 +1,100 @@ +-- supabase_monitor_admin: passwordless, read-only +-- monitoring role (quick_explainer.md, +-- migrations/db/init-scripts/00000000000000-initial-schema.sql). +-- attributes: login, no password, bypassrls, nothing else +select rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, + rolreplication, rolbypassrls, rolconnlimit, rolpassword is null as no_password +from pg_authid where rolname = 'supabase_monitor_admin'; + rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | no_password +----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+------------- + f | t | f | f | t | f | t | 2 | t +(1 row) + +-- per-role settings (read-only by default, empty search_path, timeouts) +select unnest(rolconfig) as setting from pg_roles where rolname = 'supabase_monitor_admin' order by 1; + setting +---------------------------------------- + default_transaction_read_only=on + idle_in_transaction_session_timeout=3s + lock_timeout=3s + search_path="" + statement_timeout=3s + transaction_read_only=on +(6 rows) + +-- memberships: exactly pg_monitor and pg_read_all_data +select roleid::regrole::text as role, admin_option +from pg_auth_members where member = 'supabase_monitor_admin'::regrole order by 1; + role | admin_option +------------------+-------------- + pg_monitor | f + pg_read_all_data | f +(2 rows) + +-- nobody is a member of it +select count(*) from pg_auth_members where roleid = 'supabase_monitor_admin'::regrole; + count +------- + 0 +(1 row) + +-- owns nothing and holds no object-level ACL entries anywhere +select count(*) from pg_shdepend +where refclassid = 'pg_authid'::regclass + and refobjid = 'supabase_monitor_admin'::regrole + and deptype in ('o', 'a'); + count +------- + 0 +(1 row) + +-- no CREATE on the database +select has_database_privilege('supabase_monitor_admin', current_database(), 'CREATE'); + has_database_privilege +------------------------ + f +(1 row) + +-- reads every table regardless of RLS, but cannot write. NOTE: this uses +-- SET SESSION AUTHORIZATION, which a future supautils impersonation guard +-- would block; switch to a probe role with identical grants when that lands +-- (see quick_explainer.md). +create table public.monitor_probe (id int); +insert into public.monitor_probe values (1), (2); +alter table public.monitor_probe enable row level security; +alter table public.monitor_probe force row level security; +alter table public.monitor_probe owner to postgres; +set session authorization supabase_monitor_admin; +select count(*) from public.monitor_probe; + count +------- + 2 +(1 row) + +insert into public.monitor_probe values (3); +ERROR: permission denied for table monitor_probe +reset session authorization; +drop table public.monitor_probe; +-- reserved role (supautils.conf.j2): the non-superuser postgres role cannot +-- grant it, modify it, grant to it, or drop it. Same pattern as +-- supautils_reserved_roles.sql. +begin; +set role postgres; +savepoint s; +grant supabase_monitor_admin to postgres; +ERROR: "supabase_monitor_admin" role memberships are reserved, only superusers can grant them +rollback to savepoint s; +alter role supabase_monitor_admin password 'x'; +ERROR: "supabase_monitor_admin" is a reserved role, only superusers can modify it +rollback to savepoint s; +alter role supabase_monitor_admin set search_path = public; +ERROR: "supabase_monitor_admin" is a reserved role, only superusers can modify it +rollback to savepoint s; +grant authenticated to supabase_monitor_admin; +ERROR: "supabase_monitor_admin" is a reserved role, only superusers can modify it +rollback to savepoint s; +drop role supabase_monitor_admin; +ERROR: "supabase_monitor_admin" is a reserved role, only superusers can modify it +rollback to savepoint s; +reset role; +rollback; diff --git a/nix/tests/expected/z_17_roles.out b/nix/tests/expected/z_17_roles.out index b966f8f9d5..f6c418dc69 100644 --- a/nix/tests/expected/z_17_roles.out +++ b/nix/tests/expected/z_17_roles.out @@ -70,10 +70,12 @@ order by supabase_etl_admin | pg_monitor | f supabase_etl_admin | pg_read_all_data | f supabase_etl_admin | supabase_privileged_role | f + supabase_monitor_admin | pg_monitor | f + supabase_monitor_admin | pg_read_all_data | f supabase_read_only_user | pg_monitor | f supabase_read_only_user | pg_read_all_data | f supabase_storage_admin | authenticator | f -(25 rows) +(27 rows) -- Check version-specific privileges of the roles on the schemas select schema_name, privilege_type, grantee, default_for @@ -166,10 +168,12 @@ order by supabase_etl_admin | pg_monitor | f supabase_etl_admin | pg_read_all_data | f supabase_etl_admin | supabase_privileged_role | f + supabase_monitor_admin | pg_monitor | f + supabase_monitor_admin | pg_read_all_data | f supabase_read_only_user | pg_monitor | f supabase_read_only_user | pg_read_all_data | f supabase_storage_admin | authenticator | f -(24 rows) +(26 rows) -- Check all privileges of non-superuser roles on functions select diff --git a/nix/tests/expected/z_17_supabase_monitor_admin.out b/nix/tests/expected/z_17_supabase_monitor_admin.out new file mode 100644 index 0000000000..9e7212e17d --- /dev/null +++ b/nix/tests/expected/z_17_supabase_monitor_admin.out @@ -0,0 +1,110 @@ +-- supabase_monitor_admin: passwordless, read-only +-- monitoring role (quick_explainer.md, +-- migrations/db/init-scripts/00000000000000-initial-schema.sql) +-- attributes: login, no password, bypassrls, nothing else +-- only accessible via localhost +select rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, + rolreplication, rolbypassrls, rolconnlimit, rolpassword is null as no_password +from pg_authid where rolname = 'supabase_monitor_admin'; + rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | no_password +----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+------------- + f | t | f | f | t | f | t | 2 | t +(1 row) + +-- per-role settings (read-only by default, empty search_path, timeouts) +select unnest(rolconfig) as setting from pg_roles where rolname = 'supabase_monitor_admin' order by 1; + setting +---------------------------------------- + default_transaction_read_only=on + idle_in_transaction_session_timeout=3s + lock_timeout=3s + search_path="" + statement_timeout=3s + transaction_read_only=on +(6 rows) + +-- memberships: exactly pg_monitor and pg_read_all_data (inherited; the role is +-- a plain INHERIT role). SET ROLE to them only drops privileges, checked below. +select roleid::regrole::text as role, admin_option, inherit_option, set_option +from pg_auth_members where member = 'supabase_monitor_admin'::regrole order by 1; + role | admin_option | inherit_option | set_option +------------------+--------------+----------------+------------ + pg_monitor | f | t | t + pg_read_all_data | f | t | t +(2 rows) + +-- nobody is a member of it +select count(*) from pg_auth_members where roleid = 'supabase_monitor_admin'::regrole; + count +------- + 0 +(1 row) + +-- owns nothing and holds no object-level ACL entries anywhere +-- keep in mind, the postgres role can assign it access to objects at a later point, but that's fine. +select count(*) from pg_shdepend +where refclassid = 'pg_authid'::regclass + and refobjid = 'supabase_monitor_admin'::regrole + and deptype in ('o', 'a'); + count +------- + 0 +(1 row) + +-- no CREATE on the database +select has_database_privilege('supabase_monitor_admin', current_database(), 'CREATE'); + has_database_privilege +------------------------ + f +(1 row) + +-- reads every table regardless of RLS, but cannot write; SET ROLE to a +-- predefined role only loses privileges (BYPASSRLS stays with supabase_monitor_admin, +-- so the RLS-protected rows disappear). NOTE: this uses SET SESSION +-- AUTHORIZATION, which supautils impersonation may block +create table public.monitor_probe (id int); +insert into public.monitor_probe values (1), (2); +alter table public.monitor_probe enable row level security; +alter table public.monitor_probe force row level security; +alter table public.monitor_probe owner to postgres; +set session authorization supabase_monitor_admin; +select count(*) from public.monitor_probe; + count +------- + 2 +(1 row) + +insert into public.monitor_probe values (3); +ERROR: permission denied for table monitor_probe +set role pg_read_all_data; +select count(*) from public.monitor_probe; + count +------- + 0 +(1 row) + +reset session authorization; +drop table public.monitor_probe; +-- reserved role (supautils.conf.j2): the non-superuser postgres role cannot +-- grant it, modify it, grant to it, or drop it. Same pattern as +-- supautils_reserved_roles.sql. +begin; +set role postgres; +savepoint s; +grant supabase_monitor_admin to postgres; +ERROR: "supabase_monitor_admin" role memberships are reserved, only superusers can grant them +rollback to savepoint s; +alter role supabase_monitor_admin password 'x'; +ERROR: "supabase_monitor_admin" is a reserved role, only superusers can modify it +rollback to savepoint s; +alter role supabase_monitor_admin set search_path = public; +ERROR: "supabase_monitor_admin" is a reserved role, only superusers can modify it +rollback to savepoint s; +grant authenticated to supabase_monitor_admin; +ERROR: "supabase_monitor_admin" is a reserved role, only superusers can modify it +rollback to savepoint s; +drop role supabase_monitor_admin; +ERROR: "supabase_monitor_admin" is a reserved role, only superusers can modify it +rollback to savepoint s; +reset role; +rollback; diff --git a/nix/tests/expected/z_multigres-17_roles.out b/nix/tests/expected/z_multigres-17_roles.out index 7e88404eac..1319908966 100644 --- a/nix/tests/expected/z_multigres-17_roles.out +++ b/nix/tests/expected/z_multigres-17_roles.out @@ -67,10 +67,12 @@ order by supabase_etl_admin | pg_monitor | f supabase_etl_admin | pg_read_all_data | f supabase_etl_admin | supabase_privileged_role | f + supabase_monitor_admin | pg_monitor | f + supabase_monitor_admin | pg_read_all_data | f supabase_read_only_user | pg_monitor | f supabase_read_only_user | pg_read_all_data | f supabase_storage_admin | authenticator | f -(22 rows) +(24 rows) -- Check version-specific privileges of the roles on the schemas select schema_name, privilege_type, grantee, default_for @@ -158,10 +160,12 @@ order by supabase_etl_admin | pg_monitor | f supabase_etl_admin | pg_read_all_data | f supabase_etl_admin | supabase_privileged_role | f + supabase_monitor_admin | pg_monitor | f + supabase_monitor_admin | pg_read_all_data | f supabase_read_only_user | pg_monitor | f supabase_read_only_user | pg_read_all_data | f supabase_storage_admin | authenticator | f -(21 rows) +(23 rows) -- Check all privileges of non-superuser roles on functions select diff --git a/nix/tests/expected/z_multigres-orioledb-17_roles.out b/nix/tests/expected/z_multigres-orioledb-17_roles.out index 9e530eee17..1593ae606c 100644 --- a/nix/tests/expected/z_multigres-orioledb-17_roles.out +++ b/nix/tests/expected/z_multigres-orioledb-17_roles.out @@ -45,11 +45,12 @@ order by rolname; supabase_auth_admin | t | t | f | f | f | f | -1 | f | supabase_etl_admin | f | t | f | t | f | t | -1 | t | supabase_functions_admin | t | t | f | f | f | f | -1 | f | + supabase_monitor_admin | f | t | f | t | f | f | 2 | t | supabase_privileged_role | f | f | f | t | f | f | -1 | f | supabase_read_only_user | f | t | f | t | f | f | -1 | t | supabase_replication_admin | f | t | f | t | f | t | -1 | f | supabase_storage_admin | t | t | f | f | f | f | -1 | f | -(28 rows) +(29 rows) select rolname, @@ -57,8 +58,8 @@ select from pg_roles r where rolname not in ('pg_create_subscription', 'pg_maintain', 'pg_use_reserved_connections') order by rolname; - rolname | rolconfig -----------------------------+------------------------------------------------------------------------------------------ + rolname | rolconfig +----------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------ anon | {statement_timeout=3s} authenticated | {statement_timeout=8s} authenticator | {"session_preload_libraries=supautils, safeupdate",statement_timeout=8s,lock_timeout=8s} @@ -83,11 +84,12 @@ order by rolname; supabase_auth_admin | {search_path=auth,idle_in_transaction_session_timeout=60000,log_statement=none} supabase_etl_admin | supabase_functions_admin | + supabase_monitor_admin | {default_transaction_read_only=on,transaction_read_only=on,"search_path=\"\"",statement_timeout=3s,lock_timeout=3s,idle_in_transaction_session_timeout=3s} supabase_privileged_role | supabase_read_only_user | {default_transaction_read_only=on} supabase_replication_admin | supabase_storage_admin | {search_path=storage,log_statement=none} -(28 rows) +(29 rows) -- Check all privileges of the roles on the schemas select schema_name, privilege_type, grantee, default_for diff --git a/nix/tests/sql/z_15_supabase_monitor_admin.sql b/nix/tests/sql/z_15_supabase_monitor_admin.sql new file mode 100644 index 0000000000..473432c0f0 --- /dev/null +++ b/nix/tests/sql/z_15_supabase_monitor_admin.sql @@ -0,0 +1,61 @@ +-- supabase_monitor_admin: passwordless, read-only +-- monitoring role ( +-- migrations/db/init-scripts/00000000000000-initial-schema.sql). + +-- attributes: login, no password, bypassrls, nothing else +select rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, + rolreplication, rolbypassrls, rolconnlimit, rolpassword is null as no_password +from pg_authid where rolname = 'supabase_monitor_admin'; + +-- per-role settings (read-only by default, empty search_path, timeouts) +select unnest(rolconfig) as setting from pg_roles where rolname = 'supabase_monitor_admin' order by 1; + +-- memberships: exactly pg_monitor and pg_read_all_data +select roleid::regrole::text as role, admin_option +from pg_auth_members where member = 'supabase_monitor_admin'::regrole order by 1; + +-- nobody is a member of it +select count(*) from pg_auth_members where roleid = 'supabase_monitor_admin'::regrole; + +-- owns nothing and holds no object-level ACL entries anywhere +select count(*) from pg_shdepend +where refclassid = 'pg_authid'::regclass + and refobjid = 'supabase_monitor_admin'::regrole + and deptype in ('o', 'a'); + +-- no CREATE on the database +select has_database_privilege('supabase_monitor_admin', current_database(), 'CREATE'); + +-- reads every table regardless of RLS, but cannot write. NOTE: this uses +-- SET SESSION AUTHORIZATION, which a future supautils impersonation guard +-- would block; switch to a probe role with identical grants when that lands +-- (see quick_explainer.md). +create table public.monitor_probe (id int); +insert into public.monitor_probe values (1), (2); +alter table public.monitor_probe enable row level security; +alter table public.monitor_probe force row level security; +alter table public.monitor_probe owner to postgres; +set session authorization supabase_monitor_admin; +select count(*) from public.monitor_probe; +insert into public.monitor_probe values (3); +reset session authorization; +drop table public.monitor_probe; + +-- reserved role (supautils.conf.j2): the non-superuser postgres role cannot +-- grant it, modify it, grant to it, or drop it. Same pattern as +-- supautils_reserved_roles.sql. +begin; +set role postgres; +savepoint s; +grant supabase_monitor_admin to postgres; +rollback to savepoint s; +alter role supabase_monitor_admin password 'x'; +rollback to savepoint s; +alter role supabase_monitor_admin set search_path = public; +rollback to savepoint s; +grant authenticated to supabase_monitor_admin; +rollback to savepoint s; +drop role supabase_monitor_admin; +rollback to savepoint s; +reset role; +rollback; diff --git a/nix/tests/sql/z_17_supabase_monitor_admin.sql b/nix/tests/sql/z_17_supabase_monitor_admin.sql new file mode 100644 index 0000000000..506cfdfef9 --- /dev/null +++ b/nix/tests/sql/z_17_supabase_monitor_admin.sql @@ -0,0 +1,65 @@ +-- supabase_monitor_admin: passwordless, read-only, for monitoring from localhost +-- migrations/db/init-scripts/00000000000000-initial-schema.sql) + +-- attributes: login, no password, bypassrls, nothing else +-- only accessible via localhost +select rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, + rolreplication, rolbypassrls, rolconnlimit, rolpassword is null as no_password +from pg_authid where rolname = 'supabase_monitor_admin'; + +-- per-role settings (read-only by default, empty search_path, timeouts) +select unnest(rolconfig) as setting from pg_roles where rolname = 'supabase_monitor_admin' order by 1; + +-- memberships: exactly pg_monitor and pg_read_all_data (inherited; the role is +-- a plain INHERIT role). SET ROLE to them only drops privileges, checked below. +select roleid::regrole::text as role, admin_option, inherit_option, set_option +from pg_auth_members where member = 'supabase_monitor_admin'::regrole order by 1; + +-- nobody is a member of it +select count(*) from pg_auth_members where roleid = 'supabase_monitor_admin'::regrole; + +-- owns nothing and holds no object-level ACL entries anywhere +-- keep in mind, the postgres role can assign it access to objects at a later point, but that's fine. +select count(*) from pg_shdepend +where refclassid = 'pg_authid'::regclass + and refobjid = 'supabase_monitor_admin'::regrole + and deptype in ('o', 'a'); + +-- no CREATE on the database +select has_database_privilege('supabase_monitor_admin', current_database(), 'CREATE'); + +-- reads every table regardless of RLS, but cannot write; SET ROLE to a +-- predefined role only loses privileges (BYPASSRLS stays with supabase_monitor_admin, +-- so the RLS-protected rows disappear). NOTE: this uses SET SESSION +-- AUTHORIZATION, which supautils impersonation may block +create table public.monitor_probe (id int); +insert into public.monitor_probe values (1), (2); +alter table public.monitor_probe enable row level security; +alter table public.monitor_probe force row level security; +alter table public.monitor_probe owner to postgres; +set session authorization supabase_monitor_admin; +select count(*) from public.monitor_probe; +insert into public.monitor_probe values (3); +set role pg_read_all_data; +select count(*) from public.monitor_probe; +reset session authorization; +drop table public.monitor_probe; + +-- reserved role (supautils.conf.j2): the non-superuser postgres role cannot +-- grant it, modify it, grant to it, or drop it. Same pattern as +-- supautils_reserved_roles.sql. +begin; +set role postgres; +savepoint s; +grant supabase_monitor_admin to postgres; +rollback to savepoint s; +alter role supabase_monitor_admin password 'x'; +rollback to savepoint s; +alter role supabase_monitor_admin set search_path = public; +rollback to savepoint s; +grant authenticated to supabase_monitor_admin; +rollback to savepoint s; +drop role supabase_monitor_admin; +rollback to savepoint s; +reset role; +rollback; diff --git a/testinfra/test_ami_nix.py b/testinfra/test_ami_nix.py index b5acf94c58..879e4c4c99 100644 --- a/testinfra/test_ami_nix.py +++ b/testinfra/test_ami_nix.py @@ -1157,6 +1157,45 @@ def test_postgrest_read_only_session_attrs(host): print("Warning: Failed to restore PostgreSQL configuration") +def test_supabase_monitor_admin_role_is_passwordless_and_reachable_on_loopback(host): + """supabase_monitor_admin (migrations/db/init-scripts/00000000000000-initial-schema.sql) + has no password and relies on the same pg_hba trust rules. + + Unlike the other service roles, this role must never be given a password in + nix/tools/postgresql_schema.sql or the fixture at the top of this file. + It should only be accessible from localhost. + """ + ssh = host["ssh"] + psql_admin = "sudo -u postgres psql -U supabase_admin -h localhost -d postgres -At -c " + + result = run_ssh_command( + ssh, + psql_admin + + "\"select rolcanlogin, rolpassword is null, rolbypassrls, rolsuper, " + "rolcreaterole, rolcreatedb, rolreplication " + "from pg_authid where rolname = 'supabase_monitor_admin'\"", + ) + assert result["succeeded"], result["stderr"] + assert result["stdout"].strip() == "t|t|t|f|f|f|f", ( + f"unexpected supabase_monitor_admin attributes: {result['stdout']!r}" + ) + + # Passwordless loopback login works, and the per-role read-only default + # applies to the resulting session. + result = run_ssh_command( + ssh, + "sudo -u postgres psql -U supabase_monitor_admin -h 127.0.0.1 -d postgres -At " + "-c 'select current_user' -c 'show default_transaction_read_only' 2>&1", + ) + assert result["succeeded"], ( + "supabase_monitor_admin could not log in over loopback without a password: " + f"{result['stdout']!r} {result['stderr']!r}" + ) + assert result["stdout"].split() == ["supabase_monitor_admin", "on"], ( + f"unexpected session state for supabase_monitor_admin: {result['stdout']!r}" + ) + + def test_custom_overrides_take_precedence_over_generated_optimizations(host): """Verify CLI-managed PostgreSQL config overrides generated optimizations. From 395af073c2fa966bdc710e5bc7bfce4e30b9abae Mon Sep 17 00:00:00 2001 From: Brian Date: Thu, 12 Nov 2026 19:18:42 +0900 Subject: [PATCH 2/3] fix: sync monitor role test output and apply formatter --- nix/tests/expected/z_15_supabase_monitor_admin.out | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/tests/expected/z_15_supabase_monitor_admin.out b/nix/tests/expected/z_15_supabase_monitor_admin.out index 4224791350..b6fbfdb785 100644 --- a/nix/tests/expected/z_15_supabase_monitor_admin.out +++ b/nix/tests/expected/z_15_supabase_monitor_admin.out @@ -1,5 +1,5 @@ -- supabase_monitor_admin: passwordless, read-only --- monitoring role (quick_explainer.md, +-- monitoring role ( -- migrations/db/init-scripts/00000000000000-initial-schema.sql). -- attributes: login, no password, bypassrls, nothing else select rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, From 7ff856f318648989b19e7a147e908119d404ce50 Mon Sep 17 00:00:00 2001 From: Brian Date: Thu, 12 Nov 2026 19:44:42 +0900 Subject: [PATCH 3/3] fix: sync PG17 monitor role test output and apply formatter --- nix/tests/expected/z_17_supabase_monitor_admin.out | 3 +-- testinfra/test_ami_nix.py | 8 +++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/nix/tests/expected/z_17_supabase_monitor_admin.out b/nix/tests/expected/z_17_supabase_monitor_admin.out index 9e7212e17d..c9c124091d 100644 --- a/nix/tests/expected/z_17_supabase_monitor_admin.out +++ b/nix/tests/expected/z_17_supabase_monitor_admin.out @@ -1,5 +1,4 @@ --- supabase_monitor_admin: passwordless, read-only --- monitoring role (quick_explainer.md, +-- supabase_monitor_admin: passwordless, read-only, for monitoring from localhost -- migrations/db/init-scripts/00000000000000-initial-schema.sql) -- attributes: login, no password, bypassrls, nothing else -- only accessible via localhost diff --git a/testinfra/test_ami_nix.py b/testinfra/test_ami_nix.py index 879e4c4c99..ea67dd355e 100644 --- a/testinfra/test_ami_nix.py +++ b/testinfra/test_ami_nix.py @@ -1162,16 +1162,18 @@ def test_supabase_monitor_admin_role_is_passwordless_and_reachable_on_loopback(h has no password and relies on the same pg_hba trust rules. Unlike the other service roles, this role must never be given a password in - nix/tools/postgresql_schema.sql or the fixture at the top of this file. + nix/tools/postgresql_schema.sql or the fixture at the top of this file. It should only be accessible from localhost. """ ssh = host["ssh"] - psql_admin = "sudo -u postgres psql -U supabase_admin -h localhost -d postgres -At -c " + psql_admin = ( + "sudo -u postgres psql -U supabase_admin -h localhost -d postgres -At -c " + ) result = run_ssh_command( ssh, psql_admin - + "\"select rolcanlogin, rolpassword is null, rolbypassrls, rolsuper, " + + '"select rolcanlogin, rolpassword is null, rolbypassrls, rolsuper, ' "rolcreaterole, rolcreatedb, rolreplication " "from pg_authid where rolname = 'supabase_monitor_admin'\"", )