Skip to content
Merged
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
31 changes: 31 additions & 0 deletions HISTORY.asc
Original file line number Diff line number Diff line change
@@ -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.
50 changes: 0 additions & 50 deletions sql/test_factory--0.5.0--stable.sql

This file was deleted.

87 changes: 85 additions & 2 deletions sql/test_factory--0.5.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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'));
Expand Down
2 changes: 1 addition & 1 deletion test_factory.control
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion test_factory_pgtap.control
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Loading