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/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 7eaa88a..00b6207 100644 --- a/sql/cat_tools.sql.in +++ b/sql/cat_tools.sql.in @@ -1984,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 @@ + +