Part of #93.
Permission normalization makes privileges converge between a fresh install and an updated one by construction. That is worth having, but it should be defence-in-depth rather than the only thing standing between us and privilege drift — if normalization is ever mis-specified, skipped for a new object class, or bypassed, nothing currently notices. This issue is about detecting the drift directly, so correctness does not depend on remembering to normalize.
Nothing in the repo compares privileges beyond bin/structural_diff.sql's per-member ACL columns (proacl, typacl, relacl, nspacl). That misses at least three things.
1. pg_init_privs is never compared
This is what let defect D1 through: a fresh install and an updated install had byte-identical typacl on five enum types but differed by five pg_init_privs rows, because a fresh install granted implicitly via ALTER DEFAULT PRIVILEGES (not snapshotted) while the update path granted explicitly (snapshotted). structural_diff reported IDENTICAL; pg_dump output differed by 44 lines.
pg_init_privs drives which GRANT/REVOKE statements pg_dump emits for extension members, and for an extension it is essentially the only component pg_dump sees — checkExtensionMembership() restricts extension members to DUMP_COMPONENT_ACL. So a divergence here is a real dump-fidelity divergence even when runtime privileges match.
Adding a pg_init_privs comparison to structural_diff must land after permission normalization, or it will fire on the existing divergence.
2. pg_default_acl state itself is unchecked
The ADP rows are catalog state independent of any object. Two databases can have identical objects and identical ACLs while differing in what default privileges are configured for future objects — which is a latent divergence that only manifests when the next object is created.
Worth checking as an invariant rather than only as a comparison: for every pg_default_acl entry, assert that every existing object of that class, in that schema, owned by that role, already carries the privilege. That catches ADP non-retroactivity in a single database with no second install to compare against — so it can run in every job that has a database, and against a production database. A 39-line prototype did exactly this with zero false positives across five healthy databases and exact pinpointing on a seeded one.
Note this check needs no superuser, unlike event-trigger-based approaches, so it stays compatible with #88.
3. Column-level ACLs
pg_attribute.attacl is independent of relacl and is never subsumed by a later table-level grant. Not currently used by cat_tools, but _cat_tools.catalog_metadata is a real table, so this becomes reachable the moment anyone grants at column granularity.
Related, and worth folding in
bin/structural_diff also misses the extension's own schema entirely — see #94. That is a privilege gap too: every install script grants USAGE on schema cat_tools, and nothing verifies an update script preserved it.
Part of #93.
Permission normalization makes privileges converge between a fresh install and an updated one by construction. That is worth having, but it should be defence-in-depth rather than the only thing standing between us and privilege drift — if normalization is ever mis-specified, skipped for a new object class, or bypassed, nothing currently notices. This issue is about detecting the drift directly, so correctness does not depend on remembering to normalize.
Nothing in the repo compares privileges beyond
bin/structural_diff.sql's per-member ACL columns (proacl,typacl,relacl,nspacl). That misses at least three things.1.
pg_init_privsis never comparedThis is what let defect D1 through: a fresh install and an updated install had byte-identical
typaclon five enum types but differed by fivepg_init_privsrows, because a fresh install granted implicitly viaALTER DEFAULT PRIVILEGES(not snapshotted) while the update path granted explicitly (snapshotted).structural_diffreported IDENTICAL;pg_dumpoutput differed by 44 lines.pg_init_privsdrives whichGRANT/REVOKEstatementspg_dumpemits for extension members, and for an extension it is essentially the only componentpg_dumpsees —checkExtensionMembership()restricts extension members toDUMP_COMPONENT_ACL. So a divergence here is a real dump-fidelity divergence even when runtime privileges match.Adding a
pg_init_privscomparison tostructural_diffmust land after permission normalization, or it will fire on the existing divergence.2.
pg_default_aclstate itself is uncheckedThe ADP rows are catalog state independent of any object. Two databases can have identical objects and identical ACLs while differing in what default privileges are configured for future objects — which is a latent divergence that only manifests when the next object is created.
Worth checking as an invariant rather than only as a comparison: for every
pg_default_aclentry, assert that every existing object of that class, in that schema, owned by that role, already carries the privilege. That catches ADP non-retroactivity in a single database with no second install to compare against — so it can run in every job that has a database, and against a production database. A 39-line prototype did exactly this with zero false positives across five healthy databases and exact pinpointing on a seeded one.Note this check needs no superuser, unlike event-trigger-based approaches, so it stays compatible with #88.
3. Column-level ACLs
pg_attribute.attaclis independent ofrelacland is never subsumed by a later table-level grant. Not currently used by cat_tools, but_cat_tools.catalog_metadatais a real table, so this becomes reachable the moment anyone grants at column granularity.Related, and worth folding in
bin/structural_diffalso misses the extension's own schema entirely — see #94. That is a privilege gap too: every install script grantsUSAGEon schemacat_tools, and nothing verifies an update script preserved it.