diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index be9c9a72d..2be061941 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,7 +44,7 @@ jobs: target: production build-args: | ${{ steps.args.outputs.result }} - tags: supabase/postgres:${{ steps.settings.outputs.postgres-version }} + tags: supabase/postgres:${{ steps.settings.outputs.postgres-version }},supabase_postgres cache-from: | type=gha,scope=${{ github.ref_name }}-${{ steps.settings.outputs.postgres-version }}-${{ matrix.arch }} type=gha,scope=${{ github.base_ref }}-${{ steps.settings.outputs.postgres-version }}-${{ matrix.arch }} @@ -107,10 +107,6 @@ jobs: PGUSER: supabase_admin PGPASSWORD: ${{ env.POSTGRES_PASSWORD }} - schema: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - name: verify schema.sql is committed run: | docker compose -f migrations/docker-compose.yaml up db dbmate --abort-on-container-exit diff --git a/Dockerfile-156 b/Dockerfile-156 index 162c7fff2..ce83237f2 100644 --- a/Dockerfile-156 +++ b/Dockerfile-156 @@ -206,6 +206,8 @@ STOPSIGNAL SIGINT EXPOSE 5432 ENV POSTGRES_HOST=/var/run/postgresql +ENV POSTGRES_USER=supabase_admin +ENV POSTGRES_DB=postgres RUN apt-get update && apt-get install -y --no-install-recommends \ locales \ && rm -rf /var/lib/apt/lists/* && \ @@ -219,4 +221,3 @@ ENV LC_CTYPE=C.UTF-8 ENV LC_COLLATE=C.UTF-8 ENV LOCALE_ARCHIVE /usr/lib/locale/locale-archive CMD ["postgres", "-D", "/etc/postgresql"] - diff --git a/ansible/files/admin_api_scripts/pg_upgrade_scripts/common.sh b/ansible/files/admin_api_scripts/pg_upgrade_scripts/common.sh index 02ce2c9a6..5ed134c42 100755 --- a/ansible/files/admin_api_scripts/pg_upgrade_scripts/common.sh +++ b/ansible/files/admin_api_scripts/pg_upgrade_scripts/common.sh @@ -10,6 +10,8 @@ if [ -f "$REPORTING_CREDENTIALS_FILE" ]; then REPORTING_ANON_KEY=$(cat "$REPORTING_CREDENTIALS_FILE") fi +# shellcheck disable=SC2120 +# Arguments are passed in other files function run_sql { psql -h localhost -U supabase_admin -d postgres "$@" } @@ -83,3 +85,415 @@ CI_start_postgres() { su postgres -c "$BINDIR/pg_ctl start -o '-c config_file=/etc/postgresql/postgresql.conf' -l /tmp/postgres.log" } + +swap_postgres_and_supabase_admin() { + run_sql <<'EOSQL' +begin; +create role supabase_tmp superuser; +set session authorization supabase_tmp; + +do $$ +declare + postgres_rolpassword text := (select rolpassword from pg_authid where rolname = 'postgres'); + supabase_admin_rolpassword text := (select rolpassword from pg_authid where rolname = 'supabase_admin'); + role_settings jsonb[] := ( + select coalesce(array_agg(jsonb_build_object('database', d.datname, 'role', a.rolname, 'configs', s.setconfig)), '{}') + from pg_db_role_setting s + left join pg_database d on d.oid = s.setdatabase + join pg_authid a on a.oid = s.setrole + where a.rolname in ('postgres', 'supabase_admin') + ); + event_triggers jsonb[] := (select coalesce(array_agg(jsonb_build_object('name', evtname)), '{}') from pg_event_trigger where evtowner = 'postgres'::regrole); + user_mappings jsonb[] := ( + select coalesce(array_agg(jsonb_build_object('oid', um.oid, 'role', a.rolname, 'server', s.srvname, 'options', um.umoptions)), '{}') + from pg_user_mapping um + join pg_authid a on a.oid = um.umuser + join pg_foreign_server s on s.oid = um.umserver + where a.rolname in ('postgres', 'supabase_admin') + ); + -- Objects can have initial privileges either by having those privileges set + -- when the system is initialized (by initdb) or when the object is created + -- during a CREATE EXTENSION and the extension script sets initial + -- privileges using the GRANT system. (https://www.postgresql.org/docs/current/catalog-pg-init-privs.html) + -- We only care about swapping init_privs for extensions. + init_privs jsonb[] := ( + select coalesce(array_agg(jsonb_build_object('objoid', objoid, 'classoid', classoid, 'initprivs', initprivs::text)), '{}') + from pg_init_privs + where privtype = 'e' + ); + default_acls jsonb[] := ( + select coalesce(array_agg(jsonb_build_object('oid', d.oid, 'role', a.rolname, 'schema', n.nspname, 'objtype', d.defaclobjtype, 'acl', defaclacl::text)), '{}') + from pg_default_acl d + join pg_authid a on a.oid = d.defaclrole + left join pg_namespace n on n.oid = d.defaclnamespace + ); + schemas jsonb[] := ( + select coalesce(array_agg(jsonb_build_object('oid', n.oid, 'owner', a.rolname, 'acl', nspacl::text)), '{}') + from pg_namespace n + join pg_authid a on a.oid = n.nspowner + where true + and n.nspname != 'information_schema' + and not starts_with(n.nspname, 'pg_') + ); + types jsonb[] := ( + select coalesce(array_agg(jsonb_build_object('oid', t.oid, 'owner', a.rolname, 'acl', t.typacl::text)), '{}') + from pg_type t + join pg_namespace n on n.oid = t.typnamespace + join pg_authid a on a.oid = t.typowner + where true + and n.nspname != 'information_schema' + and not starts_with(n.nspname, 'pg_') + and ( + t.typrelid = 0 + or ( + select + c.relkind = 'c' + from + pg_class c + where + c.oid = t.typrelid + ) + ) + and not exists ( + select + from + pg_type el + where + el.oid = t.typelem + and el.typarray = t.oid + ) + ); + functions jsonb[] := ( + select coalesce(array_agg(jsonb_build_object('oid', p.oid, 'owner', a.rolname, 'kind', p.prokind, 'acl', p.proacl::text)), '{}') + from pg_proc p + join pg_namespace n on n.oid = p.pronamespace + join pg_authid a on a.oid = p.proowner + where true + and n.nspname != 'information_schema' + and not starts_with(n.nspname, 'pg_') + ); + relations jsonb[] := ( + select coalesce(array_agg(jsonb_build_object('oid', c.oid, 'owner', a.rolname, 'acl', c.relacl::text)), '{}') + from ( + -- Sequences must appear after tables, so we order by relkind + select * from pg_class order by relkind desc + ) c + join pg_namespace n on n.oid = c.relnamespace + join pg_authid a on a.oid = c.relowner + where true + and n.nspname != 'information_schema' + and not starts_with(n.nspname, 'pg_') + and c.relkind not in ('c', 'i') + ); + rec record; + obj jsonb; +begin + set local search_path = ''; + + alter role postgres rename to supabase_admin_; + alter role supabase_admin rename to postgres; + alter role supabase_admin_ rename to supabase_admin; + + -- role grants + for rec in + select * from pg_auth_members + loop + execute(format('revoke %I from %I;', rec.roleid::regrole, rec.member::regrole)); + execute(format( + 'grant %I to %I %s granted by %I;', + case + when rec.roleid = 'postgres'::regrole then 'supabase_admin' + when rec.roleid = 'supabase_admin'::regrole then 'postgres' + else rec.roleid::regrole + end, + case + when rec.member = 'postgres'::regrole then 'supabase_admin' + when rec.member = 'supabase_admin'::regrole then 'postgres' + else rec.member::regrole + end, + case + when rec.admin_option then 'with admin option' + else '' + end, + case + when rec.grantor = 'postgres'::regrole then 'supabase_admin' + when rec.grantor = 'supabase_admin'::regrole then 'postgres' + else rec.grantor::regrole + end + )); + end loop; + + -- role passwords + execute(format('alter role postgres password %L;', postgres_rolpassword)); + execute(format('alter role supabase_admin password %L;', supabase_admin_rolpassword)); + + -- role settings + foreach obj in array role_settings + loop + execute(format('alter role %I %s reset all', + case when obj->>'role' = 'postgres' then 'supabase_admin' else 'postgres' end, + case when obj->>'database' is null then '' else format('in database %I', obj->>'database') end + )); + end loop; + foreach obj in array role_settings + loop + for rec in + select split_part(value, '=', 1) as key, substr(value, strpos(value, '=') + 1) as value + from jsonb_array_elements_text(obj->'configs') + loop + execute(format('alter role %I %s set %I to %s', + obj->>'role', + case when obj->>'database' is null then '' else format('in database %I', obj->>'database') end, + rec.key, + rec.value + )); + end loop; + end loop; + + reassign owned by postgres to supabase_admin; + + -- databases + for rec in + select * from pg_database where datname not in ('template0') + loop + execute(format('alter database %I owner to postgres;', rec.datname)); + end loop; + + -- event triggers + foreach obj in array event_triggers + loop + execute(format('alter event trigger %I owner to postgres;', obj->>'name')); + end loop; + + -- publications + for rec in + select * from pg_publication + loop + execute(format('alter publication %I owner to postgres;', rec.pubname)); + end loop; + + -- FDWs + for rec in + select * from pg_foreign_data_wrapper + loop + execute(format('alter foreign data wrapper %I owner to postgres;', rec.fdwname)); + end loop; + + -- foreign servers + for rec in + select * from pg_foreign_server + loop + execute(format('alter server %I owner to postgres;', rec.srvname)); + end loop; + + -- user mappings + foreach obj in array user_mappings + loop + execute(format('drop user mapping for %I server %I', case when obj->>'role' = 'postgres' then 'supabase_admin' else 'postgres' end, obj->>'server')); + end loop; + foreach obj in array user_mappings + loop + execute(format('create user mapping for %I server %I', obj->>'role', obj->>'server')); + for rec in + select split_part(value, '=', 1) as key, substr(value, strpos(value, '=') + 1) as value + from jsonb_array_elements_text(obj->'options') + loop + execute(format('alter user mapping for %I server %I options (%I %L)', obj->>'role', obj->>'server', rec.key, rec.value)); + end loop; + end loop; + + -- init privs + foreach obj in array init_privs + loop + -- We need to modify system catalog directly here because there's no ALTER INIT PRIVILEGES. + update pg_init_privs set initprivs = (obj->>'initprivs')::aclitem[] where objoid = (obj->>'objoid')::oid and classoid = (obj->>'classoid')::oid; + end loop; + + -- default acls + foreach obj in array default_acls + loop + for rec in + select grantor, grantee, privilege_type, is_grantable + from aclexplode((obj->>'acl')::aclitem[]) + loop + if obj->>'role' in ('postgres', 'supabase_admin') or rec.grantee::regrole in ('postgres', 'supabase_admin') then + execute(format('alter default privileges for role %I %s revoke %s on %s from %I' + , case when obj->>'role' = 'postgres' then 'supabase_admin' + when obj->>'role' = 'supabase_admin' then 'postgres' + else obj->>'role' + end + , case when obj->>'schema' is null then '' + else format('in schema %I', (obj->>'schema')::regnamespace) + end + , rec.privilege_type + , case when obj->>'objtype' = 'r' then 'tables' + when obj->>'objtype' = 'S' then 'sequences' + when obj->>'objtype' = 'f' then 'functions' + when obj->>'objtype' = 'T' then 'types' + when obj->>'objtype' = 'n' then 'schemas' + end + , case when rec.grantee = 'postgres'::regrole then 'supabase_admin' + when rec.grantee = 'supabase_admin'::regrole then 'postgres' + else rec.grantee::regrole + end + )); + end if; + end loop; + end loop; + + foreach obj in array default_acls + loop + for rec in + select grantor, grantee, privilege_type, is_grantable + from aclexplode((obj->>'acl')::aclitem[]) + loop + if obj->>'role' in ('postgres', 'supabase_admin') or rec.grantee::regrole in ('postgres', 'supabase_admin') then + execute(format('alter default privileges for role %I %s grant %s on %s to %I %s' + , obj->>'role' + , case when obj->>'schema' is null then '' + else format('in schema %I', (obj->>'schema')::regnamespace) + end + , rec.privilege_type + , case when obj->>'objtype' = 'r' then 'tables' + when obj->>'objtype' = 'S' then 'sequences' + when obj->>'objtype' = 'f' then 'functions' + when obj->>'objtype' = 'T' then 'types' + when obj->>'objtype' = 'n' then 'schemas' + end + , rec.grantee::regrole + , case when rec.is_grantable then 'with grant option' else '' end + )); + end if; + end loop; + end loop; + + -- schemas + foreach obj in array schemas + loop + if obj->>'owner' = 'postgres' then + execute(format('alter schema %s owner to postgres;', (obj->>'oid')::regnamespace)); + end if; + for rec in + select grantor, grantee, privilege_type, is_grantable + from aclexplode((obj->>'acl')::aclitem[]) + where grantee::regrole in ('postgres', 'supabase_admin') + loop + execute(format('revoke %s on schema %s from %I', rec.privilege_type, (obj->>'oid')::regnamespace, case when rec.grantee = 'postgres'::regrole then 'supabase_admin' else 'postgres' end)); + end loop; + end loop; + foreach obj in array schemas + loop + for rec in + select grantor, grantee, privilege_type, is_grantable + from aclexplode((obj->>'acl')::aclitem[]) + where grantee::regrole in ('postgres', 'supabase_admin') + loop + execute(format('grant %s on schema %s to %I %s', rec.privilege_type, (obj->>'oid')::regnamespace, rec.grantee::regrole, case when rec.is_grantable then 'with grant option' else '' end)); + end loop; + end loop; + + -- types + foreach obj in array types + loop + if obj->>'owner' = 'postgres' then + execute(format('alter type %s owner to postgres;', (obj->>'oid')::regtype)); + end if; + for rec in + select grantor, grantee, privilege_type, is_grantable + from aclexplode((obj->>'acl')::aclitem[]) + where grantee::regrole in ('postgres', 'supabase_admin') + loop + execute(format('revoke %s on type %s from %I', rec.privilege_type, (obj->>'oid')::regtype, case when rec.grantee = 'postgres'::regrole then 'supabase_admin' else 'postgres' end)); + end loop; + end loop; + foreach obj in array types + loop + for rec in + select grantor, grantee, privilege_type, is_grantable + from aclexplode((obj->>'acl')::aclitem[]) + where grantee::regrole in ('postgres', 'supabase_admin') + loop + execute(format('grant %s on type %s to %I %s', rec.privilege_type, (obj->>'oid')::regtype, rec.grantee::regrole, case when rec.is_grantable then 'with grant option' else '' end)); + end loop; + end loop; + + -- functions + foreach obj in array functions + loop + if obj->>'owner' = 'postgres' then + execute(format('alter routine %s(%s) owner to postgres;', (obj->>'oid')::regproc, pg_get_function_identity_arguments((obj->>'oid')::regproc))); + end if; + for rec in + select grantor, grantee, privilege_type, is_grantable + from aclexplode((obj->>'acl')::aclitem[]) + where grantee::regrole in ('postgres', 'supabase_admin') + loop + execute(format('revoke %s on %s %s(%s) from %I' + , rec.privilege_type + , case + when obj->>'kind' = 'p' then 'procedure' + else 'function' + end + , (obj->>'oid')::regproc + , pg_get_function_identity_arguments((obj->>'oid')::regproc) + , case when rec.grantee = 'postgres'::regrole then 'supabase_admin' else 'postgres' end + )); + end loop; + end loop; + foreach obj in array functions + loop + for rec in + select grantor, grantee, privilege_type, is_grantable + from aclexplode((obj->>'acl')::aclitem[]) + where grantee::regrole in ('postgres', 'supabase_admin') + loop + execute(format('grant %s on %s %s(%s) to %I %s' + , rec.privilege_type + , case + when obj->>'kind' = 'p' then 'procedure' + else 'function' + end + , (obj->>'oid')::regproc + , pg_get_function_identity_arguments((obj->>'oid')::regproc) + , rec.grantee::regrole + , case when rec.is_grantable then 'with grant option' else '' end + )); + end loop; + end loop; + + -- relations + foreach obj in array relations + loop + -- obj->>'oid' (text) needs to be casted to oid first for some reason + + if obj->>'owner' = 'postgres' then + execute(format('alter table %s owner to postgres;', (obj->>'oid')::oid::regclass)); + end if; + for rec in + select grantor, grantee, privilege_type, is_grantable + from aclexplode((obj->>'acl')::aclitem[]) + where grantee::regrole in ('postgres', 'supabase_admin') + loop + execute(format('revoke %s on table %s from %I', rec.privilege_type, (obj->>'oid')::oid::regclass, case when rec.grantee = 'postgres'::regrole then 'supabase_admin' else 'postgres' end)); + end loop; + end loop; + foreach obj in array relations + loop + -- obj->>'oid' (text) needs to be casted to oid first for some reason + + for rec in + select grantor, grantee, privilege_type, is_grantable + from aclexplode((obj->>'acl')::aclitem[]) + where grantee::regrole in ('postgres', 'supabase_admin') + loop + execute(format('grant %s on table %s to %I %s', rec.privilege_type, (obj->>'oid')::oid::regclass, rec.grantee::regrole, case when rec.is_grantable then 'with grant option' else '' end)); + end loop; + end loop; +end +$$; + +set session authorization supabase_admin; +drop role supabase_tmp; +commit; +EOSQL +} diff --git a/ansible/files/admin_api_scripts/pg_upgrade_scripts/initiate.sh b/ansible/files/admin_api_scripts/pg_upgrade_scripts/initiate.sh index 21fec9ed9..7c5abfdae 100755 --- a/ansible/files/admin_api_scripts/pg_upgrade_scripts/initiate.sh +++ b/ansible/files/admin_api_scripts/pg_upgrade_scripts/initiate.sh @@ -73,6 +73,8 @@ if [ -n "$IS_CI" ]; then echo "PGVERSION: $PGVERSION" fi +OLD_BOOTSTRAP_USER=$(run_sql -A -t -c "select rolname from pg_authid where oid = 10;") + cleanup() { UPGRADE_STATUS=${1:-"failed"} EXIT_CODE=${?:-0} @@ -367,10 +369,15 @@ function initiate_upgrade { echo "7. Disabling extensions and generating post-upgrade script" handle_extensions - - echo "8. Granting SUPERUSER to postgres user" + + echo "8.1. Granting SUPERUSER to postgres user" run_sql -c "ALTER USER postgres WITH SUPERUSER;" + if [ "$OLD_BOOTSTRAP_USER" = "postgres" ]; then + echo "8.2. Swap postgres & supabase_admin roles as we're upgrading a project with postgres as bootstrap user" + swap_postgres_and_supabase_admin + fi + if [ -z "$IS_NIX_UPGRADE" ]; then if [ -d "/usr/share/postgresql/${PGVERSION}" ]; then mv "/usr/share/postgresql/${PGVERSION}" "/usr/share/postgresql/${PGVERSION}.bak" @@ -390,9 +397,19 @@ function initiate_upgrade { rm -rf "${PGDATANEW:?}/" if [ "$IS_NIX_UPGRADE" = "true" ]; then - LC_ALL=en_US.UTF-8 LC_CTYPE=$SERVER_LC_CTYPE LC_COLLATE=$SERVER_LC_COLLATE LANGUAGE=en_US.UTF-8 LANG=en_US.UTF-8 LOCALE_ARCHIVE=/usr/lib/locale/locale-archive su -c ". /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh && $PGBINNEW/initdb --encoding=$SERVER_ENCODING --lc-collate=$SERVER_LC_COLLATE --lc-ctype=$SERVER_LC_CTYPE -L $PGSHARENEW -D $PGDATANEW/" -s "$SHELL" postgres + LC_ALL=en_US.UTF-8 LC_CTYPE=$SERVER_LC_CTYPE LC_COLLATE=$SERVER_LC_COLLATE LANGUAGE=en_US.UTF-8 LANG=en_US.UTF-8 LOCALE_ARCHIVE=/usr/lib/locale/locale-archive su -c ". /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh && $PGBINNEW/initdb --encoding=$SERVER_ENCODING --lc-collate=$SERVER_LC_COLLATE --lc-ctype=$SERVER_LC_CTYPE -L $PGSHARENEW -D $PGDATANEW/ --username=supabase_admin" -s "$SHELL" postgres else - su -c "$PGBINNEW/initdb -L $PGSHARENEW -D $PGDATANEW/" -s "$SHELL" postgres + su -c "$PGBINNEW/initdb -L $PGSHARENEW -D $PGDATANEW/ --username=supabase_admin" -s "$SHELL" postgres + fi + + # This line avoids the need to supply the supabase_admin password on the old + # instance, since pg_upgrade connects to the db as supabase_admin using unix + # sockets, which is gated behind scram-sha-256 per pg_hba.conf.j2. The new + # instance is unaffected. + if ! grep -q "local all supabase_admin trust" /etc/postgresql/pg_hba.conf; then + echo "local all supabase_admin trust +$(cat /etc/postgresql/pg_hba.conf)" > /etc/postgresql/pg_hba.conf + run_sql -c "select pg_reload_conf();" fi UPGRADE_COMMAND=$(cat < $TMPDIR/getkey.sh echo 'echo $PGSODIUM_KEY' >> $TMPDIR/getkey.sh chmod +x $TMPDIR/getkey.sh - initdb --locale=C + initdb --locale=C --username=supabase_admin substitute ${./nix/tests/postgresql.conf.in} $PGDATA/postgresql.conf \ --subst-var-by PGSODIUM_GETKEY_SCRIPT "$TMPDIR/getkey.sh" echo "listen_addresses = '*'" >> $PGDATA/postgresql.conf @@ -459,14 +459,14 @@ exit 1 fi done - createdb -p 5432 -h localhost testing - if ! psql -p 5432 -h localhost -d testing -v ON_ERROR_STOP=1 -Xaf ${./nix/tests/prime.sql}; then + createdb -p 5432 -h localhost --username=supabase_admin testing + if ! psql -p 5432 -h localhost --username=supabase_admin -d testing -v ON_ERROR_STOP=1 -Xaf ${./nix/tests/prime.sql}; then echo "Error executing SQL file. PostgreSQL log content:" cat $TMPDIR/logfile/postgresql.log pg_ctl -D "$PGDATA" stop exit 1 fi - pg_prove -p 5432 -h localhost -d testing ${sqlTests}/*.sql + pg_prove -p 5432 -h localhost --username=supabase_admin -d testing ${sqlTests}/*.sql mkdir -p $out/regression_output pg_regress \ @@ -476,6 +476,7 @@ --outputdir=$out/regression_output \ --host=localhost \ --port=5432 \ + --user=supabase_admin \ $(ls ${./nix/tests/sql} | sed -e 's/\..*$//' | sort ) pg_ctl -D "$PGDATA" stop diff --git a/migrations/db/init-scripts/00000000000000-initial-schema.sql b/migrations/db/init-scripts/00000000000000-initial-schema.sql index a98f0144d..ecce79a3d 100644 --- a/migrations/db/init-scripts/00000000000000-initial-schema.sql +++ b/migrations/db/init-scripts/00000000000000-initial-schema.sql @@ -5,7 +5,6 @@ create publication supabase_realtime; -- Supabase super admin -create user supabase_admin; alter user supabase_admin with superuser createdb createrole replication bypassrls; -- Supabase replication user diff --git a/migrations/db/migrate.sh b/migrations/db/migrate.sh index 1882978ed..0a84d1e6c 100755 --- a/migrations/db/migrate.sh +++ b/migrations/db/migrate.sh @@ -28,6 +28,16 @@ fi db=$( cd -- "$( dirname -- "$0" )" > /dev/null 2>&1 && pwd ) if [ -z "${USE_DBMATE:-}" ]; then + psql -v ON_ERROR_STOP=1 --no-password --no-psqlrc -U supabase_admin <