diff --git a/.vendor/linter b/.vendor/linter index b8632c2..e4fab69 160000 --- a/.vendor/linter +++ b/.vendor/linter @@ -1 +1 @@ -Subproject commit b8632c2a3d93de664a45f4622235871e8f19cf78 +Subproject commit e4fab69e7f6256682f5b6b80e7a8f2423a0c39a9 diff --git a/HISTORY.asc b/HISTORY.asc index 7939217..dedd1a5 100644 --- a/HISTORY.asc +++ b/HISTORY.asc @@ -1,6 +1,21 @@ STABLE ------ +### Changes + +* A fresh install and an updated install now produce identical `pg_init_privs`, + and therefore identical `pg_dump` output. A fresh install used to grant `USAGE` + on five enum types implicitly via `ALTER DEFAULT PRIVILEGES`, which is never + snapshotted into `pg_init_privs`, while an update granted the same privilege + explicitly, which is -- so a dump of a fresh install emitted five `GRANT` + statements that a dump of an updated install omitted. Both scripts now end by + calling the new `_cat_tools.privilege__normalize()`, which resets every + privilege cat_tools issues to its intended state. +* `_cat_tools` relations that previously carried no explicit ACL + (`catalog_metadata`, `pg_depend_identity_v`) now have one spelling out + owner-only access. No privilege changes hands; the effective permissions are + what they always were. + 0.3.0 ----- New functions and types for working with routines and partitioned relations. diff --git a/RELEASE.md b/RELEASE.md index 8a58948..1aed672 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,145 +1,24 @@ # Releasing cat_tools -cat_tools builds on pgxntool (https://github.com/Postgres-Extensions/pgxntool); -the release machinery (`make tag`, `make dist`) lives in `pgxntool/base.mk`. These -steps cut a new release. - -## 1. Safety check: verify committed version files haven't drifted - -Before anything else, confirm every committed versioned install script still matches -what that version actually shipped. - -- [ ] For each committed versioned install script — `sql/cat_tools--.sql.in` - (0.2.0 onward) or `sql/cat_tools--.sql` (the historical pre-0.2.0 files - `0.1.0`/`0.1.3`/`0.1.4`/`0.1.5`, which have no `.sql.in` source and are tracked - directly; see CLAUDE.md "SQL file conventions") — find its last-touching commit: - `git log -1 --format='%H %ad' -- sql/cat_tools--.sql.in` (or `.sql`). -- [ ] Confirm that commit is no later than when that version was actually tagged. - cat_tools tags every release, unprefixed (e.g. `0.2.2`, `0.2.3` — see step 6's - `make tag`), so compare directly: `git log -1 --format='%H %ad' `. (No - tags exist before `0.2.2` — `0.1.x`, `0.2.0`, and `0.2.1` predate cat_tools' - tagging convention — so this direct comparison is only available from `0.2.2` - onward.) -- [ ] A version file touched by a commit LATER than its own release's tag is a red - flag — it likely means `default_version` in `cat_tools.control` was left pointing - at that real (non-`stable`) version after release, and a later source edit to - `sql/cat_tools.sql.in` silently regenerated — and corrupted — the committed file - via `sql.mk`'s current-version rule (`$(EXTENSION__CURRENT_VERSION__FILES:.sql=.sql.in): - sql/cat_tools.sql.in cat_tools.control`, which `cp`s the base source over it). - Investigate before proceeding. -- [ ] **Known exception, not necessarily a corruption:** a version file whose - last-touching commit is much later than its version's real release can also mean - the file was legitimately backfilled or reformatted after the fact (e.g. once new - build tooling started requiring something that wasn't tracked before). A late - touch-date alone isn't suspicious — only worry about a file whose *content* - actually differs from what shipped. - -## 2. Pre-release checks -- [ ] Open issues/PRs for this release reviewed, merged or deferred. -- [ ] CI green on all supported PostgreSQL versions (the `all-checks-passed` job on master). -- [ ] Locally: `make verify-results` passes. It depends on `$(TEST_DEPS)` directly, not - on `test` itself (deliberately — see `base.mk`'s own comment on why: `test`'s early - exit on a regression would otherwise abort the chain before verify-results got to - inspect and report the diff), so it still runs the suite first, then gates on the - results with a stricter pgtap-aware check. `make test` itself also returns - non-zero on a regression as of pgxntool 2.3.0, but `verify-results` remains the - stricter, documented gate. - -## 3. Decide the version and what to track -- [ ] Pick the new version (semantic versioning). -- [ ] **Minor change? Consider NOT committing the generated versioned install script.** - If this release makes only fairly minor changes (unlikely to cross a PostgreSQL - supported-version boundary), decide whether to omit the generated - `sql/cat_tools--.sql.in` — it has little update-test-coverage value and - omitting it keeps the repo smaller (it is regenerated from `sql/cat_tools.sql.in` - at build time). The update script (`sql/cat_tools----.sql.in`) is - ALWAYS committed. See CLAUDE.md "SQL file conventions". - -## 4. Update version + changelog - -> **⚠️ CRITICAL — you are temporarily leaving the `stable` pseudo-version.** Master's -> `default_version` normally sits at the `stable` pseudo-version so that source edits -> regenerate `cat_tools--stable.sql` and never a frozen released file. Stamping a real -> version here points the generated current-version file at `cat_tools--`. The -> moment this release is merged you **MUST** flip `default_version` back to `stable` on -> master (step 7). If you forget, the next source edit on master will regenerate — and -> corrupt — the just-released version's install file. -> -> `sql/cat_tools--.sql.in` must ALWAYS be tracked — that's the entire point, -> keeping a real history of the extension across releases. `stable` is the ONE exception -> (its `.sql.in` is gitignored, not tracked) for the ONE reason that `stable` itself is -> never actually released, so there's no history to keep. - -- [ ] Bump `default_version` in `cat_tools.control` (bumped by hand). -- [ ] Bump the version in `META.in.json` — the source of truth is - `provides.cat_tools.version` (also update the top-level `version`); `META.json`, - `control.mk`, and `meta.mk` (which feeds `PGXNVERSION`) regenerate via `make`. -- [ ] Advance `release_status` in `META.in.json` as appropriate (unstable → testing → - stable). -- [ ] Add/finish the update script `sql/cat_tools----.sql.in`; confirm - `ALTER EXTENSION cat_tools UPDATE` from the previous version reaches the new one, - on multiple PG majors. -- [ ] Stamp `HISTORY.asc`: the top `STABLE` section accumulates user-facing changes as - PRs land; at release, rename that header to the new version number. - -## 5. Verify -- [ ] `make verify-results` green (it runs `test` first, then gates on the results). -- [ ] From a clean checkout (or `git archive` of the tag): `make && make install` - regenerates and installs cleanly and `CREATE EXTENSION cat_tools;` reports the new - version — confirms a PGXN consumer can build from the tracked sources alone. (This - mirrors what `make dist` ships, since it archives the tag: committed files only, so - any omitted generated install script is regenerated on the consumer's side.) - -## 6. Tag and distribute - -> **⚠️ Pass `PGXN_REMOTE=` to every target below if your clone's `origin` -> is a personal fork** (as it typically is for a maintainer working from a fork, -> with the canonical repo configured under some other remote name — check -> `git remote -v` rather than assuming it's called `upstream`). Without it, -> `tag`/`rmtag`/`forcetag`/`dist` push to `origin` by default — silently -> tagging the fork instead of `Postgres-Extensions/cat_tools`. This is exactly -> what happened when the 0.2.3 release tag was first cut and had to be -> re-pointed by hand; pgxntool 2.2.0 added `PGXN_REMOTE` specifically to fix -> this (https://github.com/Postgres-Extensions/pgxntool/issues/53). - -- [ ] Commit the release changes; working tree must be clean — `make tag` aborts with - "Untracked changes!" on a dirty tree. -- [ ] `make tag` — creates a git tag named exactly the version, UNPREFIXED (e.g. `0.2.3`, - matching the existing `0.2.2` tag; no `v` prefix), taken from `PGXNVERSION`, and - pushes it to `$(PGXN_REMOTE)` (default `origin`). It is idempotent when the tag - already points at HEAD, and errors if the tag exists on a different commit. To - move an existing tag use `make forcetag` (= `make rmtag` then `make tag`); - `make rmtag` deletes the tag locally and on `$(PGXN_REMOTE)`. -- [ ] `make dist` — depends on `tag` (and builds the HTML docs), then - `git archive`s the tag into `../cat_tools-.zip` (parent directory). - Because it archives the tag, only committed files are included. If a `.gitattributes` - exists it must be committed, or `dist` aborts (git archive only honors - `export-ignore` for committed files). `make forcedist` = `forcetag` + `dist`. -- [ ] Upload the `../cat_tools-.zip` to PGXN (manual). - -## 7. Return master to `stable` (CRITICAL — do not skip) -- [ ] As soon as the release is merged, flip `default_version` back to the `stable` - pseudo-version on master (`cat_tools.control` + `META.in.json`), open a new top - `STABLE` section in `HISTORY.asc`, and re-seed a fresh - `sql/cat_tools----stable.sql.in` update script for the next cycle. - Leaving master stamped at the real version means the next source edit regenerates - and corrupts the released version's install file. The just-released - `sql/cat_tools--.sql.in` stays tracked, permanently — nothing to clean up - or remove there; see step 4's note above. - -> The persistent `stable` pseudo-version (a permanent version literally named `stable`, -> with a live `sql/cat_tools----stable.sql.in` update script that every -> source fix targets) decouples fixes from version bumps. The machinery is built into -> `sql.mk`; it lands immediately after the 0.2.3 release, so 0.2.3 itself is the last -> release cut before the scheme exists — steps 4/7 above describe the flow from the -> next release onward. - -## Notes and caveats - -- **0.2.3 catalog-view repair / `pg_upgrade` caveat.** Databases updated from - 0.2.0/0.2.1 hold broken catalog views that fail binary `pg_upgrade` to PostgreSQL - 12+ until the extension is updated to 0.2.3 (the update rebuilds them, dropping and - recreating the public `pg_class_v`/`column`/`pg_class()` objects without `CASCADE`). - Surface this in the release notes when people may cross the PG 11 → 12+ boundary. See - README.asc "Updating the extension" and - https://github.com/Postgres-Extensions/cat_tools/pull/42. +See [`../ai/RELEASE.md`](../ai/RELEASE.md) for the actual release process. +What follows is repo-specific context that doesn't belong in that shared +doc. + +## Pre-0.2.2 releases predate tagging + +cat_tools' first git tag is `0.2.2`; `0.1.0`/`0.1.3`/`0.1.4`/`0.1.5`, `0.2.0`, +and `0.2.1` were never tagged. The shared doc's step 1 (verify committed +version files haven't drifted) calls for comparing a version file's +last-touching commit against that version's tag — there's no tag to compare +against for any of those versions. See CLAUDE.md "SQL file conventions" for +how those pre-0.2.0 versions are tracked (plain `.sql`, no `.sql.in`). + +## 0.2.3 catalog-view repair / `pg_upgrade` caveat + +Databases updated from 0.2.0/0.2.1 hold broken catalog views that fail +binary `pg_upgrade` to PostgreSQL 12+ until the extension is updated to +0.2.3 (the update rebuilds them, dropping and recreating the public +`pg_class_v`/`column`/`pg_class()` objects without `CASCADE`). Surface this +in the release notes when people may cross the PG 11 → 12+ boundary. See +README.asc "Updating the extension" and +https://github.com/Postgres-Extensions/cat_tools/pull/42. diff --git a/sql/cat_tools--0.3.0--stable.sql.in b/sql/cat_tools--0.3.0--stable.sql.in index e69de29..57b37ce 100644 --- a/sql/cat_tools--0.3.0--stable.sql.in +++ b/sql/cat_tools--0.3.0--stable.sql.in @@ -0,0 +1,166 @@ +/* + * Converge privileges between a fresh install and an updated one, down to + * pg_dump output. https://github.com/Postgres-Extensions/cat_tools/issues/93 + * + * The install script's ALTER DEFAULT PRIVILEGES grants USAGE on our types + * implicitly, and an implicit grant is never snapshotted into pg_init_privs, + * while the same grant written out in an update script is -- identical ACLs, + * different dumps. Issuing the same explicit statements at the end of both + * scripts removes the asymmetry by construction. + */ + +/* + * One row per object cat_tools issues privileges on, plus the privileges it + * intends that object to have. object_kind is the keyword GRANT and REVOKE name + * the object with; grant_privilege is what the usage role gets, or NULL for + * nothing. privilege__normalize() below is a loop over this. + * + * Only the grantees cat_tools itself issues to -- PUBLIC and the usage role -- + * are described here. Grants an administrator made to other roles are left + * alone: an update must not silently revoke them. + */ +CREATE OR REPLACE VIEW _cat_tools.privilege_target_v AS + WITH ext AS ( + SELECT oid AS extoid + , extnamespace + FROM pg_catalog.pg_extension + WHERE extname = 'cat_tools' + ) + , member AS ( + SELECT classid + , objid + FROM pg_catalog.pg_depend + WHERE refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass + AND refobjid = (SELECT extoid FROM ext) + AND deptype = 'e' + ) + /* + * The extension's own schema is created by CREATE EXTENSION itself, so it is + * not a member and has to be unioned in. + */ + SELECT 'SCHEMA'::text AS object_kind + , n.oid::pg_catalog.regnamespace::text AS object_identity + , true AS revoke_public + , true AS revoke_role + , 'USAGE'::text AS grant_privilege + FROM pg_catalog.pg_namespace n + WHERE n.oid IN ( + SELECT extnamespace FROM ext + UNION ALL + SELECT objid FROM member WHERE classid = 'pg_catalog.pg_namespace'::pg_catalog.regclass + ) + UNION ALL + /* + * PUBLIC keeps the USAGE Postgres gives it on every type: schema USAGE is what + * actually gates our types, so revoking here would tighten the documented + * model rather than normalize it. + */ + SELECT 'TYPE' + , t.oid::pg_catalog.regtype::text + , false + , true + , 'USAGE' + FROM pg_catalog.pg_type t + WHERE t.oid IN (SELECT objid FROM member WHERE classid = 'pg_catalog.pg_type'::pg_catalog.regclass) + AND t.typtype <> 'p' + -- Array types and relation rowtypes carry no ACL of their own + AND NOT EXISTS( SELECT FROM pg_catalog.pg_type e WHERE e.typarray = t.oid ) + AND ( + t.typrelid = 0 + OR 'c' = (SELECT c.relkind FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid) + ) + UNION ALL + /* + * Relations in the extension's own schema are the public API; anything in + * _cat_tools is internal and gets no grant. Sequences (relkind 'S') are + * excluded because their privilege set is unlike a table's and cat_tools has + * none; adding one means adding a branch here. + * + * Column-level privileges (pg_attribute.attacl) are likewise out of scope: + * cat_tools grants nothing at column granularity and ALTER DEFAULT PRIVILEGES + * has no column granularity either, so the drift this exists to prevent cannot + * reach them. A table-level REVOKE never subsumes column-level privileges, so + * granting at column granularity in future means handling them here too. + */ + SELECT 'TABLE' + , c.oid::pg_catalog.regclass::text + , true + , true + , CASE WHEN c.relnamespace = (SELECT extnamespace FROM ext) THEN 'SELECT' END + FROM pg_catalog.pg_class c + WHERE c.oid IN (SELECT objid FROM member WHERE classid = 'pg_catalog.pg_class'::pg_catalog.regclass) + AND c.relkind IN ('r', 'p', 'v', 'm', 'f') + UNION ALL + /* + * Which routines the usage role may execute is a per-routine decision made at + * the creation site, where __cat_tools.create_function already issues + * REVOKE-then-GRANT, so only the REVOKE from PUBLIC is asserted here. + */ + SELECT 'ROUTINE' + , p.oid::pg_catalog.regprocedure::text + , true + , false + , NULL::text + FROM pg_catalog.pg_proc p + WHERE p.oid IN (SELECT objid FROM member WHERE classid = 'pg_catalog.pg_proc'::pg_catalog.regclass) +; +COMMENT ON VIEW _cat_tools.privilege_target_v IS 'Every object cat_tools issues privileges on, and the privileges it intends.'; + +CREATE OR REPLACE FUNCTION _cat_tools.privilege__normalize( +) RETURNS void LANGUAGE plpgsql AS $body$ +DECLARE + /* + * Single point of definition for the grantee; nothing else below names a + * role. https://github.com/Postgres-Extensions/cat_tools/issues/88 may change + * how it is chosen. + */ + c_usage_role CONSTANT pg_catalog.name := 'cat_tools__usage'; + + /* + * A non-superuser install may be unable to create the role + * (https://github.com/Postgres-Extensions/cat_tools/issues/88), so its absence + * skips the role-specific statements instead of failing. + */ + c_have_role CONSTANT boolean := EXISTS( + SELECT FROM pg_catalog.pg_roles WHERE rolname = c_usage_role + ); + + r record; +BEGIN + IF NOT c_have_role THEN + RAISE DEBUG 'role % does not exist; normalizing PUBLIC privileges only', c_usage_role; + END IF; + + FOR r IN + SELECT * + FROM _cat_tools.privilege_target_v + -- Deterministic statement order, so both paths build ACL entries alike + ORDER BY object_kind, object_identity + LOOP + /* + * REVOKE first, rather than just topping the GRANT up: an ALTER DEFAULT + * PRIVILEGES configured outside this extension reaches every object a fresh + * install creates and nothing an update script touches, so whatever it added + * would otherwise be a permanent divergence. + */ + IF r.revoke_public THEN + EXECUTE format('REVOKE ALL ON %s %s FROM PUBLIC', r.object_kind, r.object_identity); + END IF; + + CONTINUE WHEN NOT c_have_role; + + IF r.revoke_role THEN + EXECUTE format('REVOKE ALL ON %s %s FROM %I', r.object_kind, r.object_identity, c_usage_role); + END IF; + + IF r.grant_privilege IS NOT NULL THEN + EXECUTE format('GRANT %s ON %s %s TO %I', r.grant_privilege, r.object_kind, r.object_identity, c_usage_role); + END IF; + END LOOP; +END +$body$; +COMMENT ON FUNCTION _cat_tools.privilege__normalize() IS 'Reset every privilege cat_tools issues to its intended state.'; + +SELECT _cat_tools.privilege__normalize(); + +-- vi: expandtab ts=2 sw=2 diff --git a/sql/cat_tools.sql.in b/sql/cat_tools.sql.in index 339ff00..00b6207 100644 --- a/sql/cat_tools.sql.in +++ b/sql/cat_tools.sql.in @@ -775,8 +775,10 @@ CREATE TYPE cat_tools.object_type AS ENUM( , 'materialized view column' , 'composite type column' , 'foreign table column' - -- pg_constraint - -- NOTE: a domain itself is considered to be a type + /* + * pg_constraint + * NOTE: a domain itself is considered to be a type + */ , 'domain constraint', 'table constraint' -- pg_proc , 'aggregate', 'function' @@ -1281,8 +1283,10 @@ $$ @generated@ --- Borrowed from newsysviews: http://pgfoundry.org/projects/newsysviews/ --- sql-lint:disable-block all: copied verbatim from newsysviews, do not reformat +/* + * Borrowed from newsysviews: http://pgfoundry.org/projects/newsysviews/ + * sql-lint:disable-block all: copied verbatim from newsysviews, do not reformat + */ CREATE OR REPLACE VIEW cat_tools.pg_all_foreign_keys AS SELECT n1.nspname AS fk_schema_name, @@ -1980,4 +1984,171 @@ DROP FUNCTION __cat_tools.create_function( ); DROP SCHEMA __cat_tools; +@generated@ + +/* + * Converge privileges between a fresh install and an updated one, down to + * pg_dump output. https://github.com/Postgres-Extensions/cat_tools/issues/93 + * + * The install script's ALTER DEFAULT PRIVILEGES grants USAGE on our types + * implicitly, and an implicit grant is never snapshotted into pg_init_privs, + * while the same grant written out in an update script is -- identical ACLs, + * different dumps. Issuing the same explicit statements at the end of both + * scripts removes the asymmetry by construction. + */ + +/* + * One row per object cat_tools issues privileges on, plus the privileges it + * intends that object to have. object_kind is the keyword GRANT and REVOKE name + * the object with; grant_privilege is what the usage role gets, or NULL for + * nothing. privilege__normalize() below is a loop over this. + * + * Only the grantees cat_tools itself issues to -- PUBLIC and the usage role -- + * are described here. Grants an administrator made to other roles are left + * alone: an update must not silently revoke them. + */ +CREATE OR REPLACE VIEW _cat_tools.privilege_target_v AS + WITH ext AS ( + SELECT oid AS extoid + , extnamespace + FROM pg_catalog.pg_extension + WHERE extname = 'cat_tools' + ) + , member AS ( + SELECT classid + , objid + FROM pg_catalog.pg_depend + WHERE refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass + AND refobjid = (SELECT extoid FROM ext) + AND deptype = 'e' + ) + /* + * The extension's own schema is created by CREATE EXTENSION itself, so it is + * not a member and has to be unioned in. + */ + SELECT 'SCHEMA'::text AS object_kind + , n.oid::pg_catalog.regnamespace::text AS object_identity + , true AS revoke_public + , true AS revoke_role + , 'USAGE'::text AS grant_privilege + FROM pg_catalog.pg_namespace n + WHERE n.oid IN ( + SELECT extnamespace FROM ext + UNION ALL + SELECT objid FROM member WHERE classid = 'pg_catalog.pg_namespace'::pg_catalog.regclass + ) + UNION ALL + /* + * PUBLIC keeps the USAGE Postgres gives it on every type: schema USAGE is what + * actually gates our types, so revoking here would tighten the documented + * model rather than normalize it. + */ + SELECT 'TYPE' + , t.oid::pg_catalog.regtype::text + , false + , true + , 'USAGE' + FROM pg_catalog.pg_type t + WHERE t.oid IN (SELECT objid FROM member WHERE classid = 'pg_catalog.pg_type'::pg_catalog.regclass) + AND t.typtype <> 'p' + -- Array types and relation rowtypes carry no ACL of their own + AND NOT EXISTS( SELECT FROM pg_catalog.pg_type e WHERE e.typarray = t.oid ) + AND ( + t.typrelid = 0 + OR 'c' = (SELECT c.relkind FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid) + ) + UNION ALL + /* + * Relations in the extension's own schema are the public API; anything in + * _cat_tools is internal and gets no grant. Sequences (relkind 'S') are + * excluded because their privilege set is unlike a table's and cat_tools has + * none; adding one means adding a branch here. + * + * Column-level privileges (pg_attribute.attacl) are likewise out of scope: + * cat_tools grants nothing at column granularity and ALTER DEFAULT PRIVILEGES + * has no column granularity either, so the drift this exists to prevent cannot + * reach them. A table-level REVOKE never subsumes column-level privileges, so + * granting at column granularity in future means handling them here too. + */ + SELECT 'TABLE' + , c.oid::pg_catalog.regclass::text + , true + , true + , CASE WHEN c.relnamespace = (SELECT extnamespace FROM ext) THEN 'SELECT' END + FROM pg_catalog.pg_class c + WHERE c.oid IN (SELECT objid FROM member WHERE classid = 'pg_catalog.pg_class'::pg_catalog.regclass) + AND c.relkind IN ('r', 'p', 'v', 'm', 'f') + UNION ALL + /* + * Which routines the usage role may execute is a per-routine decision made at + * the creation site, where __cat_tools.create_function already issues + * REVOKE-then-GRANT, so only the REVOKE from PUBLIC is asserted here. + */ + SELECT 'ROUTINE' + , p.oid::pg_catalog.regprocedure::text + , true + , false + , NULL::text + FROM pg_catalog.pg_proc p + WHERE p.oid IN (SELECT objid FROM member WHERE classid = 'pg_catalog.pg_proc'::pg_catalog.regclass) +; +COMMENT ON VIEW _cat_tools.privilege_target_v IS 'Every object cat_tools issues privileges on, and the privileges it intends.'; + +CREATE OR REPLACE FUNCTION _cat_tools.privilege__normalize( +) RETURNS void LANGUAGE plpgsql AS $body$ +DECLARE + /* + * Single point of definition for the grantee; nothing else below names a + * role. https://github.com/Postgres-Extensions/cat_tools/issues/88 may change + * how it is chosen. + */ + c_usage_role CONSTANT pg_catalog.name := 'cat_tools__usage'; + + /* + * A non-superuser install may be unable to create the role + * (https://github.com/Postgres-Extensions/cat_tools/issues/88), so its absence + * skips the role-specific statements instead of failing. + */ + c_have_role CONSTANT boolean := EXISTS( + SELECT FROM pg_catalog.pg_roles WHERE rolname = c_usage_role + ); + + r record; +BEGIN + IF NOT c_have_role THEN + RAISE DEBUG 'role % does not exist; normalizing PUBLIC privileges only', c_usage_role; + END IF; + + FOR r IN + SELECT * + FROM _cat_tools.privilege_target_v + -- Deterministic statement order, so both paths build ACL entries alike + ORDER BY object_kind, object_identity + LOOP + /* + * REVOKE first, rather than just topping the GRANT up: an ALTER DEFAULT + * PRIVILEGES configured outside this extension reaches every object a fresh + * install creates and nothing an update script touches, so whatever it added + * would otherwise be a permanent divergence. + */ + IF r.revoke_public THEN + EXECUTE format('REVOKE ALL ON %s %s FROM PUBLIC', r.object_kind, r.object_identity); + END IF; + + CONTINUE WHEN NOT c_have_role; + + IF r.revoke_role THEN + EXECUTE format('REVOKE ALL ON %s %s FROM %I', r.object_kind, r.object_identity, c_usage_role); + END IF; + + IF r.grant_privilege IS NOT NULL THEN + EXECUTE format('GRANT %s ON %s %s TO %I', r.grant_privilege, r.object_kind, r.object_identity, c_usage_role); + END IF; + END LOOP; +END +$body$; +COMMENT ON FUNCTION _cat_tools.privilege__normalize() IS 'Reset every privilege cat_tools issues to its intended state.'; + +SELECT _cat_tools.privilege__normalize(); + -- vi: expandtab ts=2 sw=2 diff --git a/test/build/expected/build.out b/test/build/expected/build.out index abc561f..eaef812 100644 --- a/test/build/expected/build.out +++ b/test/build/expected/build.out @@ -136,6 +136,8 @@ + +