Webhook icon
Schedule icon
Script icon
Process icon
If icon
Fail icon
Query icon
Queries icon
SlackIncomingWebhook icon
Pause icon
Loop icon
Sequential icon
Set icon
Exit icon

Reconcile PostgreSQL Grants Against a YAML Spec and Revoke Extras Only After Approval

Detect missing and extra PostgreSQL grants against a YAML spec, apply the missing ones in one transaction, and revoke extras only after approval.

Categories
DataInfrastructure

Privileges drift. A migration forgets a grant, a developer runs a manual GRANT to fix an incident and never removes it, and a role ends up SUPERUSER after a debugging session. The spec in git says what should be true; the catalog says what is. This blueprint compares the two and acts on the difference.

How it works

  1. after_migration (Webhook) or hourly_drift_check (Schedule, shipped disabled) starts a run.
  2. parse_spec (Script) parses the YAML with PyYAML and checks names and privilege names. spec_guard (If) fails the run on an invalid spec.
  3. check_server and check_objects confirm the database is reachable and that every role and schema in the spec exists. objects_guard (If) fails the run otherwise.
  4. diff reads the catalog (aclexplode(relacl), pg_default_acl, pg_auth_members, pg_roles) and classifies every difference:
    • apply: a missing table or sequence privilege, a missing default privilege, a missing membership;
    • approve: a privilege or membership held by a managed role that the spec does not list;
    • report: a role with SUPERUSER or BYPASSRLS that is not in privileged_roles, or a role in forbidden_roles.
  5. apply_missing (Queries, transaction) runs every missing grant in one transaction. Any failure rolls all of them back.
  6. ask_on_call (SlackIncomingWebhook) lists the extras. wait_for_decision (Pause, 2 hours, MULTISELECT whose choices are the same labels, built by diff through expression) asks which to revoke.
  7. reread reads the catalog again. Only chosen extras that still exist are revoked, one Loop iteration each, with allowFailure.
  8. recheck reads the catalog once more, and state_after and two kv.Set tasks store fingerprints, so the same finding is not sent again.
  9. notify_outcome reports what was revoked, what was already gone, and what is still present.

Error handling

  • Invalid input stops before the database. A bad YAML, an unknown key, an invalid privilege name or a role used in a grant without being listed fails at spec_guard.
  • Missing objects stop the run. A role or schema named in the spec that does not exist fails at objects_guard. The flow never creates roles.
  • Missing grants are atomic. All GRANT and ALTER DEFAULT PRIVILEGES statements run in one Queries transaction, so a permission error in the middle leaves none of them applied.
  • Revokes are per item. Each REVOKE runs in its own Sequential with allowFailure. A failed revoke is reported as still present, and the other revokes proceed.
  • Approval never acts on stale data. The catalog is read again after the approval. An extra someone removed while the approval waited is reported as "already gone" and not touched.
  • Quoted names. Every identifier in a generated statement comes from format('%I'). No name is concatenated into SQL.
  • Alert once per change. The report and the extras are fingerprinted and stored in the KV store. An unchanged finding is silent, and a declined set is not offered again until it changes.
  • Failures alert with the cause. The errors block names the failing task and its error.

What this blueprint teaches about Kestra

  • A Pause whose MULTISELECT choices come from an expression computed earlier in the run. The chosen labels are checked against a fresh read before anything is revoked.
  • Queries with transaction: true for a batch of DDL-like statements that must succeed together.
  • Passing JSON from one task into the parameters of the next, so one SQL text serves the diff, the re-read and the final check.
  • Using Python only for the part SQL cannot do (YAML), and installing its one dependency inside the task.
  • A KV store as small state across executions, with If gates that send nothing when nothing changed.

Prerequisites

  • Tested on PostgreSQL 16. MAINTAIN (a table privilege) needs PostgreSQL 17 or later.
  • A Python 3 interpreter on the worker that can run pip3 install --break-system-packages (for PyYAML).
  • A role for POSTGRES_USER that owns the objects, or is a member of the owning role. A role cannot grant what it does not own or hold with grant option.
  • A Slack incoming webhook.

Secrets

  • POSTGRES_URL, POSTGRES_USER, POSTGRES_PASSWORD: the database to reconcile. Use a non-superuser role.
  • SLACK_WEBHOOK_URL: Slack incoming webhook.

Inputs

  • desired_grants (STRING, YAML): the spec. Schemas, roles and grants are listed by name; anything else a listed role holds in a listed schema is an extra.
  • kestra_url (STRING, default http://localhost:8080): base URL for the approval link.

Outputs

  • findings (JSON): the final catalog findings after the run. Flow outputs are not set on a failed run; the alert names the task and the error.

Quick start

  1. Edit desired_grants to describe the schemas and roles you manage. Start with one schema.
  2. Run the flow once. In sync, it stays silent.
  3. Add the webhook call to the end of your migration job.
  4. Enable hourly_drift_check when you want the backstop.

How to read the result

  • apply items are already done: the message lists them.
  • report items need a human, because they are roles, not grants.
  • approve items are the extras. They are the same labels the approval form offers.

How to extend

  • Views and functions are not covered. Covering them means extending the catalog query and the spec parser.
  • Keep several specs, one per database, by copying the flow and changing the secrets.
  • Send the report to a ticket instead of Slack by swapping notify_report.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.