From 68c2a47ac25f45c9bd0760fd4da1be0c5ba41cb9 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Thu, 8 Oct 2026 14:24:15 -0500 Subject: [PATCH] Release 0.5.0 0.5.0 was the in-development version and never reached PGXN, so release it with everything since: test_factory 0.5.0 is regenerated from current source and its 0.5.0 -> stable update script is dropped. test_factory_pgtap stays at 0.1.0 (its stable update script is a no-op), so its default_version reverts to 0.1.0. Add HISTORY.asc with the 0.5.0 changelog. Co-Authored-By: Claude Opus 5.5 --- HISTORY.asc | 31 ++++++++++ sql/test_factory--0.5.0--stable.sql | 50 ----------------- sql/test_factory--0.5.0.sql | 87 ++++++++++++++++++++++++++++- test_factory.control | 2 +- test_factory_pgtap.control | 2 +- 5 files changed, 118 insertions(+), 54 deletions(-) create mode 100644 HISTORY.asc delete mode 100644 sql/test_factory--0.5.0--stable.sql diff --git a/HISTORY.asc b/HISTORY.asc new file mode 100644 index 0000000..39e956c --- /dev/null +++ b/HISTORY.asc @@ -0,0 +1,31 @@ +0.5.0 +----- +Distribution 0.5.0 ships `test_factory` 0.5.0 and `test_factory_pgtap` 0.1.0. + +NOTE: There is no `ALTER EXTENSION` update path to 0.5.0. That covers both +`test_factory` 0.3.1 (from distribution 0.4.2) and unreleased 0.5.0 installs +taken from git; drop and recreate the extension instead. + +== Install without a superuser (`superuser = false`) +Both extensions can now be installed by a role with `CREATEROLE` and +`CREATE` on the target database, e.g. RDS/Aurora's master user, instead of +requiring a real superuser. + +== Fix `CREATE EXTENSION test_factory` on PG16+ for non-superusers +On PG16+ a non-superuser install failed with `must be able to SET ROLE +"test_factory__owner"`: `CREATE ROLE` doesn't give its creator SET-enabled +membership in the new role. Install now grants it when missing, and raises +a clear error with a suggested `GRANT` when the installer can't grant it +itself. Fixes #14. + +== Fix install failures caused by the `original_role` temp table +Both install scripts saved the caller's role in an `ON COMMIT DROP` temp +table, which became extension-owned and broke `CREATE EXTENSION +test_factory_pgtap` on PG17 (#12) and inside an explicit transaction (#13). +They now use a transaction-local setting instead. `test_factory_pgtap`'s +0.1.0 install script was fixed in place: the objects it creates are +unchanged, so its version stays 0.1.0. + +== Point META resources at the current repository +Homepage, bug tracker and repository links now point at +https://github.com/Postgres-Extensions/test_factory. diff --git a/sql/test_factory--0.5.0--stable.sql b/sql/test_factory--0.5.0--stable.sql deleted file mode 100644 index 7ba91f7..0000000 --- a/sql/test_factory--0.5.0--stable.sql +++ /dev/null @@ -1,50 +0,0 @@ -/* - * https://github.com/Postgres-Extensions/test_factory/issues/14: whoever - * originally installed 0.5.0 never automatically got SET-enabled (PG16+) - * or even plain (pre-16) membership in test_factory__owner -- that's the - * bug, and it applies just as much to an already-existing 0.5.0 install - * as to a fresh one (see sql/test_factory.sql's own comment for the fresh - * case). This grants it retroactively, so whoever runs this update -- and - * anyone else who later needs to SET ROLE test_factory__owner -- has it. - * - * No object in this extension changed between 0.5.0 and here, so unlike - * the fresh-install script, there's nothing to create or alter AS - * test_factory__owner, and so no role to switch to or restore. - */ -DO $body$ -BEGIN - IF current_setting('server_version_num')::int >= 160000 THEN - IF NOT pg_has_role(current_user, 'test_factory__owner', 'SET') THEN - BEGIN - EXECUTE format('GRANT test_factory__owner TO %I WITH SET TRUE', current_user); - EXCEPTION - WHEN insufficient_privilege THEN - RAISE EXCEPTION - 'role "%" lacks SET-enabled membership in "test_factory__owner", and lacks ADMIN OPTION to grant it to itself' - , current_user - USING ERRCODE = 'insufficient_privilege' - , HINT = format( - 'Ask a superuser, or a role with ADMIN OPTION on "test_factory__owner", to run: %s' - , format('GRANT test_factory__owner TO %I WITH SET TRUE;', current_user) - ); - END; - END IF; - ELSIF NOT pg_has_role(current_user, 'test_factory__owner', 'MEMBER') THEN - BEGIN - EXECUTE format('GRANT test_factory__owner TO %I', current_user); - EXCEPTION - WHEN insufficient_privilege THEN - RAISE EXCEPTION - 'role "%" is not a member of "test_factory__owner", and lacks ADMIN OPTION to grant it to itself' - , current_user - USING ERRCODE = 'insufficient_privilege' - , HINT = format( - 'Ask a superuser, or a role with ADMIN OPTION on "test_factory__owner", to run: %s' - , format('GRANT test_factory__owner TO %I;', current_user) - ); - END; - END IF; -END -$body$; - --- vi: expandtab ts=2 sw=2 diff --git a/sql/test_factory--0.5.0.sql b/sql/test_factory--0.5.0.sql index 37e742b..626e421 100644 --- a/sql/test_factory--0.5.0.sql +++ b/sql/test_factory--0.5.0.sql @@ -6,6 +6,7 @@ * it's much lighter weight than creating a table. */ SELECT pg_catalog.set_config('test_factory.original_role', current_user, true); + DO $body$ BEGIN CREATE ROLE test_factory__owner; @@ -15,6 +16,86 @@ EXCEPTION END $body$; +/* + * CREATE ROLE alone never grants the creator any relationship to the role + * it just created -- on ANY version, confirmed directly against a genuine + * non-superuser CREATEROLE installer with zero prior grants: SET ROLE + * test_factory__owner below fails outright ("permission denied to set + * role") without an explicit GRANT first, even immediately after this + * same session created it. A real superuser bypasses the SET ROLE check + * entirely regardless of any of this, which is why this was easy to miss + * without testing against a genuine non-superuser role. + * + * As of PG16, plain membership isn't enough either -- SET ROLE requires + * the SET option specifically, which needs GRANT ... WITH SET TRUE. + * Before PG16, plain membership (no WITH SET syntax, which doesn't exist + * yet) already confers the ability to SET ROLE. pg_has_role's 'SET' + * privilege type is PG16+ only, but as a plain function argument (not a + * catalog column reference) it's safe to call inside this same + * version-gated branch, unlike a static reference to + * pg_auth_members.set_option, which would fail to parse on older servers + * even inside the gate. + * + * Either way, skip the GRANT when it's not actually needed: an installer + * might already have the membership it needs some other way -- e.g. a DBA + * pre-provisioned the role and granted it directly, deliberately + * withholding ADMIN OPTION as a least-privilege measure -- and GRANT ROLE + * requires ADMIN OPTION on the target role (or superuser) to run AT ALL, + * regardless of whether it would end up a no-op; forcing it unconditionally + * broke that already-working case (confirmed against a live non-superuser + * role with SET but not ADMIN OPTION: "ERROR: permission denied to grant + * role ... Only roles with the ADMIN option ... may grant this role"). + * + * If the installer has neither the membership it needs nor ADMIN OPTION to + * grant it themselves, installation genuinely cannot proceed -- nobody + * else can do it on their behalf from inside this script. Catch that + * specific case and say so plainly instead of surfacing Postgres's generic + * permission error. + */ +DO $body$ +BEGIN + IF current_setting('server_version_num')::int >= 160000 THEN + IF NOT pg_has_role(current_user, 'test_factory__owner', 'SET') THEN + BEGIN + EXECUTE format('GRANT test_factory__owner TO %I WITH SET TRUE', current_user); + EXCEPTION + WHEN insufficient_privilege THEN + /* + * RAISE's own %-substitution is plain string + * interpolation, not format()'s %I/%L -- build the + * copy-pastable suggested command with format() first + * (so the role name is properly identifier-quoted), + * then substitute the whole result in with a single, + * ordinary %. + */ + RAISE EXCEPTION + 'role "%" lacks SET-enabled membership in "test_factory__owner", and lacks ADMIN OPTION to grant it to itself' + , current_user -- unquoted; the "" above is the only quoting + USING ERRCODE = 'insufficient_privilege' + , HINT = format( + 'Ask a superuser, or a role with ADMIN OPTION on "test_factory__owner", to run: %s' + , format('GRANT test_factory__owner TO %I WITH SET TRUE;', current_user) + ); + END; + END IF; + ELSIF NOT pg_has_role(current_user, 'test_factory__owner', 'MEMBER') THEN + BEGIN + EXECUTE format('GRANT test_factory__owner TO %I', current_user); + EXCEPTION + WHEN insufficient_privilege THEN + RAISE EXCEPTION + 'role "%" is not a member of "test_factory__owner", and lacks ADMIN OPTION to grant it to itself' + , current_user -- unquoted; the "" above is the only quoting + USING ERRCODE = 'insufficient_privilege' + , HINT = format( + 'Ask a superuser, or a role with ADMIN OPTION on "test_factory__owner", to run: %s' + , format('GRANT test_factory__owner TO %I;', current_user) + ); + END; + END IF; +END +$body$; + CREATE SCHEMA tf AUTHORIZATION test_factory__owner; COMMENT ON SCHEMA tf IS $$Test factory. Tools for maintaining test data.$$; GRANT USAGE ON SCHEMA tf TO public; @@ -247,8 +328,10 @@ BEGIN END $body$; ---select (tf.get('moo','moo')::moo).*; --- Restore the caller's role saved at the top of this script. +/* + * select (tf.get('moo','moo')::moo).*; + * Restore the caller's role saved at the top of this script. + */ DO $body$ BEGIN EXECUTE 'SET ROLE ' || pg_catalog.quote_ident(pg_catalog.current_setting('test_factory.original_role')); diff --git a/test_factory.control b/test_factory.control index 446c18a..b05ee89 100644 --- a/test_factory.control +++ b/test_factory.control @@ -1,5 +1,5 @@ comment = 'A framework for managing test data' -default_version = 'stable' +default_version = '0.5.0' relocatable = false # Not a security boundary weakening: test_factory__owner is a locked-down, # dedicated owner role, and every privileged function is SECURITY DEFINER diff --git a/test_factory_pgtap.control b/test_factory_pgtap.control index f19f82f..0711698 100644 --- a/test_factory_pgtap.control +++ b/test_factory_pgtap.control @@ -1,5 +1,5 @@ comment = 'A framework for managing test data' -default_version = 'stable' +default_version = '0.1.0' relocatable = false requires = 'pgtap, test_factory' # See test_factory.control -- same rationale.