Skip to content

Detect privilege drift directly, so permission normalization is defence-in-depth #95

Description

@jnasbyupgrade

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    update-safetyALTER EXTENSION UPDATE correctness: fresh-vs-updated equivalence

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions