Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0c404e3
Add extension_custom_scripts_path scripts for ownership/grant handoffs
moizpgedge Sep 8, 2026
e93955e
Generalize extension-custom-scripts: pg_database_owner, spock check
moizpgedge Sep 8, 2026
4268f67
Fix postgis_topology's after-create.sql to cover the full lifecycle
moizpgedge Sep 8, 2026
035a176
Remove lolor's before-create.sql, now handled by an event trigger
moizpgedge Sep 8, 2026
b0fbabe
Document why extension-custom-scripts can't cover a role-independent …
moizpgedge Sep 8, 2026
cf13df0
Fix lolor's trust flag instead of relying on session scoping
moizpgedge Sep 8, 2026
2cbba3a
Gate restricted extensions with a role check instead of session scoping
moizpgedge Sep 10, 2026
f167734
Force row level security on cron.job so its own owner can't bypass it
moizpgedge Sep 10, 2026
fd85f16
fix: remove role-check before-create.sql scripts, gate is session-scoped
moizpgedge Sep 14, 2026
3e8bd95
fix(address_standardizer_data_us): grant against the extension's real…
moizpgedge Sep 16, 2026
0c6d4c9
docs(lolor): record that before-create.sql does fire despite no allow…
moizpgedge Sep 16, 2026
8a312d4
test: exercise the gate and extension-custom-scripts, not just that s…
moizpgedge Sep 16, 2026
b8744fe
fix(address_standardizer_data_us): grant schema USAGE, not just table…
moizpgedge Sep 16, 2026
ac319d8
docs(Dockerfile): clarify the custom-scripts path is inert until conf…
moizpgedge Sep 16, 2026
47bfa1b
fix(pg_cron): drop ownership and FORCE RLS, SELECT is enough
moizpgedge Sep 16, 2026
5c2daad
fix(pg_cron,postgis): review follow-ups, extension-custom-scripts tes…
moizpgedge Sep 17, 2026
5352166
fix: grant reference-data access to PUBLIC
moizpgedge Sep 17, 2026
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
14 changes: 14 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,20 @@ EOF
ENV PGDATA=/var/lib/pgsql/${POSTGRES_MAJOR_VERSION}/data
RUN install --verbose --directory --owner postgres --group postgres --mode 1777 "$PGDATA"

# supautils.extension_custom_scripts_path scripts. Baked into the image
# rather than a runtime mount: supautils reads these from a plain
# filesystem path with no other configuration hook available. See
# extension-custom-scripts/README.md for the convention new scripts
# follow.
#
# This only places the scripts at a well-known path; it does not load
# supautils or point supautils.extension_custom_scripts_path at this
# directory. Neither is set anywhere in this image, deliberately, the
# same as every other supautils.* setting here: shared_preload_libraries
# is empty by default, so a deployment that wants any of this configures
# it itself in its own postgresql.conf or equivalent.
COPY --chown=postgres:postgres extension-custom-scripts /etc/pgedge/extension-custom-scripts
Comment thread
coderabbitai[bot] marked this conversation as resolved.

USER postgres

ENV PG_MAJOR=${POSTGRES_MAJOR_VERSION}
Expand Down
28 changes: 28 additions & 0 deletions extension-custom-scripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# extension-custom-scripts

Scripts for `supautils.extension_custom_scripts_path`, baked into the
`standard` image at `/etc/pgedge/extension-custom-scripts`. supautils
runs these around `CREATE EXTENSION`, as the superuser session it
already switches to for a privileged install (see
`supautils.superuser`), so a script here can assume superuser
privileges, not just the installing role's own.

Layout, per [supautils' own convention](https://github.com/supabase/supautils#readme):

```
extension-custom-scripts/
<extension-name>/
before-create.sql # optional, runs before CREATE EXTENSION
after-create.sql # optional, runs after CREATE EXTENSION
```

This image is not exclusive to any one deployment's role model, and an
extension can be installed into any database, owned by whatever role
happens to own it. A script granting access to "the role that should be
able to use this" should grant to
[`pg_database_owner`](https://www.postgresql.org/docs/current/predefined-roles.html#PREDEFINED-ROLE-PG-DATABASE-OWNER),
not a hardcoded role name: Postgres automatically maintains membership
in this predefined role to match whoever currently owns the database,
so the grant keeps working even if that database is later reassigned
to a different owner, and needs no assumption about what the owner is
named. See `pg_cron`'s `after-create.sql` for the pattern.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
-- us_lex/us_gaz/us_rules land wherever the extension itself was
-- installed, owned by the supautils superuser. Unlike the other five
-- scripts in this directory, this extension is relocatable
-- (control file has no fixed schema), so a caller can run
-- CREATE EXTENSION address_standardizer_data_us SCHEMA gis and these
-- three tables land in gis, not public. A hardcoded public.us_lex
-- here would silently fail against relations that don't exist in
-- that case, which is why this looks the schema up at runtime rather
-- than assuming it.
--
-- Looked up via pg_extension.extnamespace rather than supautils' own
-- @extschema@ substitution: that token is only populated when the
-- caller's CREATE EXTENSION included an explicit SCHEMA clause, and
-- is otherwise substituted as SQL NULL, which is the common case
-- (no explicit SCHEMA at all). pg_extension.extnamespace is populated
-- unconditionally, by Postgres itself, once the extension exists, so
-- it covers both cases with the same query.
--
-- Granted to pg_database_owner rather than a hardcoded role name, so
-- this keeps working if the database is later reassigned to a
-- different owner. See
-- https://www.postgresql.org/docs/current/predefined-roles.html.
--
-- Also grants USAGE on the schema itself, not just SELECT on the
-- tables: table-level SELECT alone is not enough to query a table
-- outside the search path, Postgres separately checks USAGE on the
-- schema before it will even look a table up in it. The default,
-- unrelocated case (public) happens to work without this, since
-- public grants USAGE to PUBLIC by default, but a schema named on an
-- explicit SCHEMA clause has no such default and would otherwise
-- leave pg_database_owner with a grant it can never actually use.
--
-- Also granted to PUBLIC: pg_database_owner's own grant carries no
-- GRANT OPTION, so there is no way to pass it on to another role
-- afterward, and the attempt is a silent no-op, not an error.
DO $$
DECLARE ext_schema name;
BEGIN
SELECT n.nspname INTO ext_schema
FROM pg_catalog.pg_extension e
JOIN pg_catalog.pg_namespace n ON n.oid = e.extnamespace
WHERE e.extname = 'address_standardizer_data_us';

EXECUTE format(
'GRANT USAGE ON SCHEMA %I TO pg_database_owner, PUBLIC',
ext_schema
);
EXECUTE format(
'GRANT SELECT ON TABLE %I.us_lex, %I.us_gaz, %I.us_rules TO pg_database_owner, PUBLIC',
ext_schema, ext_schema, ext_schema
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
END
$$;
26 changes: 26 additions & 0 deletions extension-custom-scripts/pg_cron/after-create.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-- supautils runs this immediately after CREATE EXTENSION pg_cron, as the
-- same superuser session used to install the extension (see
-- supautils.superuser). pg_cron's install script creates cron.job and
-- cron.job_run_details owned by that superuser, leaving the current
-- database's own owner with no path to manage its own scheduled jobs
-- or review their run history.
--
-- Granted to pg_database_owner rather than a hardcoded role name:
-- Postgres automatically maintains membership in this predefined role to
-- match whoever currently owns the database pg_cron was installed in, so
-- this keeps working correctly if that database is later reassigned to a
-- different owner, and needs no assumption about what that owner is
-- named. See https://www.postgresql.org/docs/current/predefined-roles.html.
--
-- SELECT only, ownership stays with the installing superuser:
-- cron.schedule() and cron.unschedule() are not SECURITY DEFINER, they
-- run as the caller, but they write to cron.job through pg_cron's own
-- internal C code, not through a normal caller-privileged INSERT or
-- UPDATE. Confirmed directly: a role with only this SELECT grant can
-- schedule, list, and unschedule its own jobs through those functions,
-- and a raw INSERT or UPDATE against cron.job as that role is refused
-- outright, permission denied, with no ownership or row-level-security
-- involved at all. Nothing here ever needs ownership to work.
GRANT USAGE ON SCHEMA cron TO pg_database_owner;
GRANT SELECT ON cron.job TO pg_database_owner;
GRANT SELECT ON cron.job_run_details TO pg_database_owner;
38 changes: 38 additions & 0 deletions extension-custom-scripts/pg_tokenizer/after-create.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
-- Read access to admin-configured tokenizer definitions
-- (tokenizer_catalog.*), not write: whoever configures tokenizers
-- stays a separate, more privileged concern. Scoped to this one
-- schema, not a database-wide default, so a future gated extension's
-- own schema isn't exposed without its own deliberate grant here.
--
-- Also granted to PUBLIC: pg_database_owner's own grant carries no
-- GRANT OPTION, so there is no way to pass it on to another role
-- afterward, and the attempt is a silent no-op, not an error. The
-- write side stays restricted to pg_database_owner only.
GRANT USAGE ON SCHEMA tokenizer_catalog TO pg_database_owner, PUBLIC;
GRANT SELECT ON ALL TABLES IN SCHEMA tokenizer_catalog TO pg_database_owner, PUBLIC;
ALTER DEFAULT PRIVILEGES FOR ROLE CURRENT_USER IN SCHEMA tokenizer_catalog
GRANT SELECT ON TABLES TO pg_database_owner, PUBLIC;

-- Schema USAGE does not just unlock reading the tables above, it
-- makes every function in this schema callable by any role, since
-- Postgres grants EXECUTE on new functions to PUBLIC by default. Most
-- of what lives here manages tokenizer/model configuration
-- (create_*, drop_*, add_preload_model, and friends), none of it
-- SECURITY DEFINER, so the table writes those functions attempt are
-- still refused on ACL, confirmed directly. But create_huggingface_model
-- and create_lindera_model run real work, parsing a config and
-- attempting to load a model, before any permission check fires, and
-- a role with only schema USAGE can reach them now. Revokes EXECUTE
-- from PUBLIC on everything in the schema, keeps it for
-- pg_database_owner explicitly rather than leaving it dependent on
-- the PUBLIC default just revoked, then re-grants PUBLIC only the
-- three functions the read-only use case actually needs: tokenize()
-- and apply_text_analyzer() to process text against an existing
-- configuration, and list_preload_models() to see what is available.
-- Configuring a new tokenizer, model, or analyzer stays a privileged
-- operation.
REVOKE EXECUTE ON ALL FUNCTIONS IN SCHEMA tokenizer_catalog FROM PUBLIC;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA tokenizer_catalog TO pg_database_owner;
GRANT EXECUTE ON FUNCTION tokenizer_catalog.tokenize(text, text) TO PUBLIC;
GRANT EXECUTE ON FUNCTION tokenizer_catalog.apply_text_analyzer(text, text) TO PUBLIC;
GRANT EXECUTE ON FUNCTION tokenizer_catalog.list_preload_models() TO PUBLIC;
12 changes: 12 additions & 0 deletions extension-custom-scripts/postgis_tiger_geocoder/after-create.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- Read access to the reference tables the extension's own functions
-- query (tiger.*, created at CREATE EXTENSION time). The actual
-- Census dataset a user bulk-loads afterward is a separate step this
-- script cannot reach, it only runs once, at CREATE EXTENSION time.
--
-- Also granted to PUBLIC: pg_database_owner's own grant carries no
-- GRANT OPTION, so there is no way to pass it on to another role
-- afterward, and the attempt is a silent no-op, not an error.
GRANT USAGE ON SCHEMA tiger TO pg_database_owner, PUBLIC;
GRANT SELECT ON ALL TABLES IN SCHEMA tiger TO pg_database_owner, PUBLIC;
ALTER DEFAULT PRIVILEGES FOR ROLE CURRENT_USER IN SCHEMA tiger
GRANT SELECT ON TABLES TO pg_database_owner, PUBLIC;
27 changes: 27 additions & 0 deletions extension-custom-scripts/postgis_topology/after-create.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-- PostGIS's own install script grants PUBLIC only read access to the
-- topology schema (USAGE on the schema, SELECT on its tables), enough
-- to read an existing topology, not to manage one. The extension's
-- actual purpose needs a lot more than INSERT on topology.topology and
-- topology.layer: DropTopology()/DropTopoGeometryColumn() DELETE from
-- both, RenameTopology()/RenameTopoGeometryColumn() UPDATE them, and
-- RenameTopoGeometryColumn() additionally runs ALTER TABLE ...
-- DISABLE/ENABLE TRIGGER on topology.layer, which Postgres never
-- grants, only an owner (or superuser) can do it. Unlike pg_cron,
-- whose write functions bypass ACL checks through internal C code,
-- postgis_topology's functions run as the caller through ordinary
-- ACL-checked DML, so a grant-only approach can't cover the trigger
-- toggle: confirmed directly, a role with full DML and even the
-- TRIGGER privilege on both tables still gets "must be owner of table
-- layer" from RenameTopoGeometryColumn(). Reassigning ownership of
-- exactly these two tables is the only way to cover all of that.
-- Scoped to exactly these two tables rather than the whole schema, so
-- it doesn't also hand write access to other gated extensions'
-- catalogs that are meant to stay admin-only.
--
-- Reassigned to pg_database_owner rather than a hardcoded role name,
-- so this keeps working if the database is later reassigned to a
-- different owner. See
-- https://www.postgresql.org/docs/current/predefined-roles.html.
ALTER TABLE topology.topology OWNER TO pg_database_owner;
ALTER TABLE topology.layer OWNER TO pg_database_owner;
GRANT USAGE ON SEQUENCE topology.topology_id_seq TO pg_database_owner;
10 changes: 10 additions & 0 deletions extension-custom-scripts/vchord_bm25/after-create.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- USAGE on bm25_catalog is needed to declare a column of its
-- bm25vector type and call its functions (search_bm25query,
-- to_bm25query); Postgres already grants EXECUTE on new functions to
-- PUBLIC by default, so no separate function grant is needed. The
-- schema holds only the type and its support functions, no tables.
--
-- Also granted to PUBLIC: pg_database_owner's own grant carries no
-- GRANT OPTION, so there is no way to pass it on to another role
-- afterward, and the attempt is a silent no-op, not an error.
GRANT USAGE ON SCHEMA bm25_catalog TO pg_database_owner, PUBLIC;
12 changes: 6 additions & 6 deletions scripts/build_pgedge_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,47 +163,47 @@ def make_all_flavor_images(
*make_all_flavor_images(
postgres_version="16.15",
spock_version="5.0.11",
epoch=2,
epoch=3,
is_latest_for_pg_major=True,
is_latest_for_spock_major=True,
),
# pg17 images
*make_all_flavor_images(
postgres_version="17.11",
spock_version="5.0.11",
epoch=2,
epoch=3,
is_latest_for_pg_major=True,
is_latest_for_spock_major=True,
),
# pg18 images
*make_all_flavor_images(
postgres_version="18.6",
spock_version="5.0.11",
epoch=2,
epoch=3,
is_latest_for_pg_major=True,
is_latest_for_spock_major=True,
),
# pg16 spock60 images
*make_all_flavor_images(
postgres_version="16.15",
spock_version="6.0.0-beta1",
epoch=2,
epoch=3,
is_latest_for_pg_major=True,
is_latest_for_spock_major=True,
),
# pg17 spock60 images
*make_all_flavor_images(
postgres_version="17.11",
spock_version="6.0.0-beta1",
epoch=2,
epoch=3,
is_latest_for_pg_major=True,
is_latest_for_spock_major=True,
),
# pg18 spock60 images
*make_all_flavor_images(
postgres_version="18.6",
spock_version="6.0.0-beta1",
epoch=2,
epoch=3,
is_latest_for_pg_major=True,
is_latest_for_spock_major=True,
),
Expand Down
Loading
Loading