Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions ansible/files/postgresql_config/supautils.conf.j2
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
supautils.extensions_parameter_overrides = '{"pg_cron":{"schema":"pg_catalog"}}'
supautils.extensions_parameter_overrides = '{"pg_cron":{"schema":"pg_catalog"},"amcheck":{"schema":"pg_catalog"}}'
supautils.policy_grants = '{"postgres":["auth.audit_log_entries","auth.flow_state","auth.identities","auth.instances","auth.mfa_amr_claims","auth.mfa_challenges","auth.mfa_factors","auth.oauth_clients","auth.one_time_tokens","auth.refresh_tokens","auth.saml_providers","auth.saml_relay_states","auth.sessions","auth.sso_domains","auth.sso_providers","auth.users","realtime.messages","realtime.subscription","storage.buckets","storage.buckets_analytics","storage.objects","storage.prefixes","storage.s3_multipart_uploads","storage.s3_multipart_uploads_parts"]}'
supautils.drop_trigger_grants = '{"postgres":["auth.audit_log_entries","auth.flow_state","auth.identities","auth.instances","auth.mfa_amr_claims","auth.mfa_challenges","auth.mfa_factors","auth.oauth_clients","auth.one_time_tokens","auth.refresh_tokens","auth.saml_providers","auth.saml_relay_states","auth.sessions","auth.sso_domains","auth.sso_providers","auth.users","realtime.messages","realtime.subscription","storage.buckets","storage.buckets_analytics","storage.objects","storage.prefixes","storage.s3_multipart_uploads","storage.s3_multipart_uploads_parts"]}'
# full list: address_standardizer, address_standardizer_data_us, adminpack, amcheck, autoinc, bloom, btree_gin, btree_gist, citext, cube, dblink, dict_int, dict_xsyn, earthdistance, file_fdw, fuzzystrmatch, hstore, http, hypopg, index_advisor, insert_username, intagg, intarray, isn, lo, ltree, moddatetime, old_snapshot, orioledb, pageinspect, pg_buffercache, pg_cron, pg_freespacemap, pg_graphql, pg_hashids, pg_jsonschema, pg_net, pg_prewarm, pg_repack, pg_stat_monitor, pg_stat_statements, pg_surgery, pg_tle, pg_trgm, pg_visibility, pg_walinspect, pgaudit, pgcrypto, pgjwt, pgmq, pgroonga, pgroonga_database, pgrouting, pgrowlocks, pgsodium, pgstattuple, pgtap, plcoffee, pljava, plls, plpgsql, plpgsql_check, plv8, postgis, postgis_raster, postgis_sfcgal, postgis_tiger_geocoder, postgis_topology, postgres_fdw, refint, rum, seg, sslinfo, supabase_vault, supautils, tablefunc, tcn, timescaledb, tsm_system_rows, tsm_system_time, unaccent, uuid-ossp, vector, wrappers, xml2
# omitted because may be unsafe: adminpack, amcheck, file_fdw, lo, old_snapshot, pageinspect, pg_freespacemap, pg_surgery, pg_visibility
# omitted because may be unsafe: adminpack, file_fdw, lo, old_snapshot, pageinspect, pg_freespacemap, pg_surgery, pg_visibility
# omitted because deprecated: intagg, xml2
# omitted because doesn't require superuser: pgmq
# omitted because protected: plpgsql
# NOTE: keep nix/tests/prime-superuser.sql in sync with the "may be unsafe" + "deprecated" lists above.
supautils.privileged_extensions = 'address_standardizer, address_standardizer_data_us, autoinc, bloom, btree_gin, btree_gist, citext, cube, dblink, dict_int, dict_xsyn, earthdistance, fuzzystrmatch, hstore, http, hypopg, index_advisor, insert_username, intarray, isn, ltree, moddatetime, orioledb, pg_buffercache, pg_cron, pg_graphql, pg_hashids, pg_jsonschema, pg_net, pg_prewarm, pg_repack, pg_stat_monitor, pg_stat_statements, pg_tle, pg_trgm, pg_walinspect, pgaudit, pgcrypto, pgjwt, pgroonga, pgroonga_database, pgrouting, pgrowlocks, pgsodium, pgstattuple, pgtap, plcoffee, pljava, plls, plpgsql_check, plv8, postgis, postgis_raster, postgis_sfcgal, postgis_tiger_geocoder, postgis_topology, postgres_fdw, refint, rum, seg, sslinfo, supabase_vault, supautils, tablefunc, tcn, timescaledb, tsm_system_rows, tsm_system_time, unaccent, uuid-ossp, vector, wrappers'
supautils.privileged_extensions = 'address_standardizer, address_standardizer_data_us, amcheck, autoinc, bloom, btree_gin, btree_gist, citext, cube, dblink, dict_int, dict_xsyn, earthdistance, fuzzystrmatch, hstore, http, hypopg, index_advisor, insert_username, intarray, isn, ltree, moddatetime, orioledb, pg_buffercache, pg_cron, pg_graphql, pg_hashids, pg_jsonschema, pg_net, pg_prewarm, pg_repack, pg_stat_monitor, pg_stat_statements, pg_tle, pg_trgm, pg_walinspect, pgaudit, pgcrypto, pgjwt, pgroonga, pgroonga_database, pgrouting, pgrowlocks, pgsodium, pgstattuple, pgtap, plcoffee, pljava, plls, plpgsql_check, plv8, postgis, postgis_raster, postgis_sfcgal, postgis_tiger_geocoder, postgis_topology, postgres_fdw, refint, rum, seg, sslinfo, supabase_vault, supautils, tablefunc, tcn, timescaledb, tsm_system_rows, tsm_system_time, unaccent, uuid-ossp, vector, wrappers'
supautils.extension_custom_scripts_path = '/etc/postgresql-custom/extension-custom-scripts'
supautils.restrict_extension_versions = 'warn'
supautils.privileged_extensions_superuser = 'supabase_admin'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
do $$
declare
saved_search_path text := (select current_setting('search_path'));
r record;
begin
perform set_config('search_path', '', true);

for r in
select p.oid::regprocedure as sig
from pg_depend d
join pg_proc p on p.oid = d.objid and d.classid = 'pg_proc'::regclass
join pg_extension e on e.oid = d.refobjid
where e.extname = 'amcheck' and d.deptype = 'e'
loop
execute format('grant execute on function %s to postgres', r.sig);
end loop;

perform set_config('search_path', saved_search_path, true);
end $$;
6 changes: 3 additions & 3 deletions ansible/vars.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ postgres_major:
# This is the source of truth for Postgres versions used in the Dockerfiles, and
# is used to derive image tags and base images in the release matrix.
postgres_release:
postgresorioledb-17: "17.9.0.018-orioledb"
postgres17: "17.6.1.165"
postgres15: "15.14.1.165"
postgresorioledb-17: "17.9.0.018-orioledb-amcheck-1"
postgres17: "17.6.1.165-amcheck-1"
postgres15: "15.14.1.165-amcheck-1"
# Docker release matrix — base images built first, layered images built on top.
# tag and base_tag are derived at build time from postgres_release via release_key.
# tag_suffix is appended to the release version to form the final image tag.
Expand Down
1 change: 1 addition & 0 deletions nix/checks.nix
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@
"pg_net_worker_privileges" # needs the authenticated/postgres roles from the full migrations, not present in the CLI prime file
"pg_cron_trigger_privileges" # needs pg_cron + the postgres role and cron-schema grants from the full migrations, not in the CLI prime file
"supautils_restrict_versions" # needs the postgres role + primed hstore from the full migrations/prime, not present in the CLI variant
"amcheck" # needs the postgres/anon/authenticated/service_role roles and the default privileges from the full migrations, plus amcheck primed by prime.sql
# Version-specific extension tests
"z_17_ext_interface"
"z_17_pg_stat_monitor"
Expand Down
191 changes: 191 additions & 0 deletions nix/ext/tests/amcheck-upgrade.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# amcheck across a 15 to 17 pg_upgrade (PSQL-1327).
#
# amcheck is pinned to the pg_catalog schema by
# supautils.extensions_parameter_overrides, which is what keeps EXECUTE away from
# anon, authenticated and service_role. nix/tests/sql/amcheck.sql asserts that on
# a fresh install.
#
# This test covers what pg_regress cannot reach: whether the schema and the
# grants survive a major version upgrade. Two things could break it. Extension
# member function ACLs ride on pg_init_privs through pg_upgrade's dump and
# restore. And 15 ships amcheck 1.3 while 17 ships 1.4, so the upgrade also runs
# an ALTER EXTENSION UPDATE that creates new functions, each picking up whatever
# default privileges apply at that moment.
#
# Upgraded projects are the population the ticket is about (customers hitting
# corrupt indexes after a 15 to 17 upgrade), so a silent re-grant here would undo
# the fix for exactly the people it was written for.
#
# 15 to 17 only. There is no upgrade path into orioledb, since its specialisation
# wipes the data directory, so an orioledb leg would assert nothing here.
{ self, pkgs }:
let
testLib = import ./lib.nix { inherit self pkgs; };
in
pkgs.testers.runNixOSTest {
name = "amcheck-upgrade";
nodes.server =
{ ... }:
{
imports = [
(testLib.makeSupabaseTestConfig {
majorVersion = "15";
})
];

specialisation.postgresql17.configuration = testLib.makeUpgradeSpecialisation {
fromMajorVersion = "15";
toMajorVersion = "17";
};
};
testScript =
{ nodes, ... }:
let
pg17-configuration = "${nodes.server.system.build.toplevel}/specialisation/postgresql17";
in
''
pg17_configuration = "${pg17-configuration}"

# postgres holds EXECUTE on amcheck (installed into pg_catalog by supautils);
# the three PostgREST roles hold nothing. Aggregated over every function
# the extension owns, so it holds as amcheck grows from 6 on 15 to 8 on 17.
EXPECTED_ACL = "anon,f,f\nauthenticated,f,f\npostgres,t,t\nservice_role,f,f"

ACL_QUERY = (
"select r.rolname, "
"bool_and(has_function_privilege(r.rolname, p.oid, 'execute')), "
"bool_or(has_function_privilege(r.rolname, p.oid, 'execute')) "
"from pg_proc p "
"join pg_depend d on d.objid = p.oid and d.deptype = 'e' "
"join pg_extension e on e.oid = d.refobjid and e.extname = 'amcheck' "
"cross join (values ('postgres'), ('anon'), ('authenticated'), ('service_role')) "
"as r(rolname) group by r.rolname order by r.rolname"
)

IDENTITY_QUERY = (
"select extversion, extnamespace::regnamespace "
"from pg_extension where extname = 'amcheck'"
)

def sql(query, role=None):
# Connect as supabase_admin (the superuser this harness authenticates
# as) and set the target role via the startup packet, so each statement
# gets its own session instead of one implicit multi-statement -c.
prefix = ""
if role is not None:
prefix = "PGOPTIONS=\"-c role=" + role + "\" "
return server.succeed(
prefix
+ "psql -U supabase_admin -d postgres -t -A -F, -v ON_ERROR_STOP=1 -c \""
+ query
+ "\""
).strip()

def sql_fails(query, role=None):
prefix = ""
if role is not None:
prefix = "PGOPTIONS=\"-c role=" + role + "\" "
return server.fail(
prefix
+ "psql -U supabase_admin -d postgres -t -A -v ON_ERROR_STOP=1 -c \""
+ query
+ "\" 2>&1"
)

start_all()

# Wait for full Supabase initialization (postgres + init-scripts + migrations),
# since the roles and default privileges under test come from the migrations.
server.wait_for_unit("supabase-db-init.service")

with subtest("Preconditions"):
assert sql("select rolsuper from pg_roles where rolname = 'postgres'") == "f", (
"postgres is a superuser; every grant assertion below would be vacuous"
)
# Fail loudly if role switching is broken, rather than silently
# running the whole suite as supabase_admin.
whoami = sql("select current_user", role="postgres")
assert whoami == "postgres", f"expected to be acting as postgres, got: {whoami}"

with subtest("A non-superuser installs amcheck on PG 15, pinned to pg_catalog"):
sql("create extension amcheck", role="postgres")
before = sql(IDENTITY_QUERY)
assert before.endswith(",pg_catalog"), (
f"expected amcheck in the pg_catalog schema, got: {before}"
)
print(f"PG 15 amcheck: {before}")

with subtest("PG 15 grants reach postgres only"):
acl_before = sql(ACL_QUERY)
assert acl_before == EXPECTED_ACL, f"unexpected PG 15 grants:\n{acl_before}"

with subtest("switch to postgresql 17"):
server.execute(f"{pg17_configuration}/bin/switch-to-configuration test >&2")
server.wait_for_unit("postgresql.service")

with subtest("pg_upgrade preserves amcheck's version, schema and grants"):
after = sql(IDENTITY_QUERY)
assert after == before, f"amcheck changed across pg_upgrade: {before} -> {after}"
acl_after = sql(ACL_QUERY)
assert acl_after == EXPECTED_ACL, f"grants changed across pg_upgrade:\n{acl_after}"

with subtest("The 1.3 -> 1.4 extension update preserves the grants"):
# The platform replays pg_upgrade's generated update_extensions.sql
# (admin_api_scripts/pg_upgrade_scripts/complete.sh). This harness runs
# raw pg_upgrade, so replay that file if present, else ALTER explicitly.
# Either route creates the new 1.4 functions, which is what we inspect.
has_script = server.succeed(
"test -f /var/lib/postgresql/update_extensions.sql && echo yes || echo no"
).strip()
if has_script == "yes":
server.succeed(
"psql -U supabase_admin -d postgres -v ON_ERROR_STOP=1 "
"-f /var/lib/postgresql/update_extensions.sql"
)
else:
sql("alter extension amcheck update")

default_version = sql(
"select default_version from pg_available_extensions where name = 'amcheck'"
)
updated = sql(IDENTITY_QUERY)
installed_version = updated.split(",")[0]
assert installed_version == default_version, (
f"expected amcheck at PG 17's default {default_version}, got {installed_version}"
)
# If 15 and 17 ever ship the same amcheck version there are no new
# functions to acquire grants, and this subtest silently tests nothing.
assert installed_version != before.split(",")[0], (
f"amcheck was already at {installed_version} on PG 15; this subtest no "
"longer exercises a version bump and needs rewriting"
)
assert updated.endswith(",pg_catalog"), (
f"amcheck left the pg_catalog schema during the update: {updated}"
)

acl_updated = sql(ACL_QUERY)
assert acl_updated == EXPECTED_ACL, (
f"grants changed when amcheck was updated to {installed_version}:\n{acl_updated}"
)

with subtest("The support workflow still works after the upgrade"):
sql(
"create table amcheck_heap(i int primary key) using heap; "
"insert into amcheck_heap select generate_series(1, 100)",
role="postgres",
)
sql("select pg_catalog.bt_index_check('amcheck_heap_pkey'::regclass)", role="postgres")
# amcheck has no per relation gate, which is the point: a customer can
# check an auth index they do not own after an upgrade corrupts it.
sql("select pg_catalog.bt_index_check('auth.users_pkey'::regclass)", role="postgres")

with subtest("The API roles are still locked out after the upgrade"):
for role in ["anon", "authenticated", "service_role"]:
err = sql_fails(
"select pg_catalog.bt_index_check('auth.users_pkey'::regclass)", role=role
)
assert "permission denied for function bt_index_check" in err, (
f"expected {role} to be denied, got: {err}"
)
'';
}
Loading
Loading