Releases: thepartly/pgroles
Release list
v0.10.0-alpha.1
Added
-
PostgresPolicyCandidate: propose and review policy content without touching the live policy. A candidate points at an existingPostgresPolicyand carries only proposed content — roles, grants, memberships. Everything about execution (interval, mode, approval and, unlessspec.targetoverrides it for a preview, the connection) comes from the policy it points at. Once created, a candidate cannot be edited — the API server rejects the write — so the version reviewed is exactly the version approved. To revise a proposal, file a successor:apiVersion: pgroles.io/v1alpha1 kind: PostgresPolicyCandidate metadata: generateName: orders-change- spec: policyRef: name: orders replaces: orders-change-x7k2p # marks the earlier draft superseded content: roles: - name: reporting_reader login: true
-
Candidates are planned inside the parent policy's reconcile. Each open candidate gets its own
PostgresPolicyPlan, computed with the parent's credentials and locks against post-enforcement database state, and reviewed and decided exactly like any other plan. Candidate planning never writes: no SQL in any state, and no generated-password Secrets. While the parent is failing or has a plan of its own awaiting a decision, candidates wait withReady=False, reason=BlockedByActivePolicy; an active ephemeral grant that touches a candidate's effects sends its plan back for fresh review withOverlayOverlap. (#182, #173) -
Promotion: merging an approved candidate's content makes its reviewed plan the one that executes. When a policy's content digest matches an approved open candidate, the operator adopts that candidate's plan — it never mints an approval of its own — and executes only if the effects recomputed under the lock still match the digest that was approved. Anything that is not a clean promotion is reported on the candidate rather than ignored: merged without approval (
PromotedWithoutApproval, the ordinary manual flow takes over), content edited after approval (PromotionDigestMismatch, nothing executes and the message says the merged spec is not being enforced), or a parent inmode: observe(PromotionNotExecuted). The candidate docs give thekubectland CI recipes for the whole flow. (#182, #173) -
pgroles candidatecovers the review side of the candidate workflow, so proposing and reading a change no longer means hand-rolling jsonpath.createfiles a candidate from the ordinary manifest your PR promotes — validated locally through the same path aspgroles validate, so a proposal the API server would reject on size bounds fails on your machine with the same field-level message, and created withgenerateNameso two people filing against one policy never collide.listshows every candidate for a policy with its phase, digest, plan and condition reasons;statusexpands one of them down to its plan's decision, who made it, whether it is still current, and what promotion had to say;diffprints the SQL approving it would run, reading the gzipped ConfigMap when the plan is too large to inline. Each command fails with a specific reason rather than printing something misreadable — a plan that stores only a truncated preview is an error, not a short diff. Deciding a plan stayskubectl-shaped on purpose: it is a status write gated by admission sodecidedByrecords an authenticated identity, and a CLI verb would blur who authenticated it. (#189) -
Approvals are bound to the database they were reviewed against, not just the Secret that reaches it. Every plan records the server's physical identity (
pg_control_system().system_identifier, the storage lineage) and a logical fingerprint of the resolved host, port and database, and both are part of the approval digest. If either changes between approval and execution — or the physical identifier was readable at approval and is not at execution — the plan is superseded instead of executed. Setspec.connection.requirePhysicalIdentity: trueto stop reconciliation entirely (TargetIdentityBlocked) when the identifier cannot be read, e.g. on engines that only speak the PostgreSQL protocol. (#180, #173) -
Owner-wide default privileges.
default_privilegesentries acceptscope: {type: global}beside the existingschema:shorthand, emittingALTER DEFAULT PRIVILEGES FOR ROLE ...with noIN SCHEMAclause. PostgreSQL keeps default privileges in two layers, and only the global one applies to every schema an owner creates objects in — including schemas no policy manages. Inspection reads the global layer for exactly the(owner, object type)pairs a manifest declares, reporting the effective default so a database with no explicitpg_default_aclrow still compares against what PostgreSQL will apply. Owner self-entries are excluded, because everyALTER DEFAULT PRIVILEGESmaterializes the owner's implicit self-grant into the stored row and reporting it would make authoritative mode revoke the owner's own default on the next reconcile. Global changes are counted on their own line indiffoutput, and in a bundle only the document owning the owner role may declare them. See default privileges. -
ensure: absentand a typedPUBLICgrantee. PostgreSQL grantsEXECUTEon every function toPUBLICwithout writing an ACL entry, so no combination of positive grants could take it away — aSECURITY DEFINERroutine stayed callable by every role. Grant entries and default-privilege entries now acceptensure: absent, which revokes a privilege where it is held, androle: PUBLICaddresses the pseudo-role (rendered unquoted, never as the identifier"PUBLIC"). Inspection reports PUBLIC's effective privileges, synthesizingacldefault(...)where the ACL is still NULL, so a fresh database plans the revoke it needs. Pair an object-level absence rule with a global default-privilege one to cover both today's objects and tomorrow's. PUBLIC is reconciled only where a rule names it, in every mode: a PUBLIC privilege no rule mentions is never revoked, and deleting apresentPUBLIC rule does not revoke it — switch the rule toensure: absent.additiveignores absence assertions with a warning, since it never revokes;adoptandauthoritativeapply them. A profile is an additive template, so a profile grant or default privilege that setsensure: absentis rejected by name instead of expanding to its opposite. Preflight warns ondiffand dry runs, and blocks a real apply, when the executor cannot act as a default-privilege owner or cannot revoke on objects it does not own — a PUBLIC revoke without that authority silently changes nothing and would otherwise re-plan forever. See grants and default privileges.
Changed
-
Plan retention is bounded per phase, so replan churn no longer evicts the record of what ran. Terminal plans were trimmed as one pool of 10 by creation time.
Supersededis generated churn — every replan supersedes its predecessor — so on an active policy it filled the pool and deleted theAppliedplans, which are the audit record of what actually executed against the database. The least informative state was evicting the most informative one. The bounds are nowApplied25 (never fewer than 30 days' worth, hard ceiling 200),FailedandRejected10 shared,Superseded3;Pending,ApprovedandApplyingare live and never evicted. The age floor makes the retained span a stated period rather than a function of how often a policy applies, and the ceiling stops that promise becoming unbounded growth.pgroles.io/keep=truestill exempts a plan from every bound. Each bound is operator-level configuration —PLAN_RETENTION_APPLIED,PLAN_RETENTION_APPLIED_MIN_AGE,PLAN_RETENTION_APPLIED_CEILING,PLAN_RETENTION_DECIDED,PLAN_RETENTION_SUPERSEDEDon the operator environment, replacing themax_plansparameter that nothing could ever set — and an invalid value refuses operator startup with the variable named. Deliberately not aPostgresPolicyfield: retention caps object growth in the cluster, and the per-object need is what thekeeplabel is for. TheAppliedbounds measure — and order — bystatus.appliedAt, not object creation, so a plan that waited on a reviewer is not already outside its floor the moment it executes. They also govern terminal-candidate pruning: deleting a candidate cascades to the plan it owns, so a promoted candidate owning anAppliedplan is held to theAppliedbounds instead of the flat terminal-candidate bound, andpgroles.io/keep=trueon either the candidate or its plan exempts the pair. (#194) -
Bundle plan JSON is now
pgroles.bundle_plan.v2. Default-privilege changes and their ownership keys carry a taggedscope({"type": "schema", "schema": "app"}or{"type": "global"}) in place of the bareschemastring, which could not express a global rule. Migration: readscope.schemawhere you readschema, and handlescope.type == "global"entries having no schema at all. -
diff --format jsoncarries the same taggedscopeon default-privilege changes. Unlike bundle output it has noschema_versionfield to bump, so nothing announces the change in the payload itself. Migration: the same one as above — readscope.schemawhere you readschema, and handlescope.type == "global"entries having no schema. This output is a bare array of changes and stays unversioned for now, so treat its shape as unstable and pin the pgroles version if you parse it. -
Database grants now name the connected database explicitly.
object.nameis required fortype: database, and inspecti...
0.9.0
Added
- Bounded, request-driven PostgreSQL memberships in Kubernetes.
EphemeralAccessPolicydefines a requestable bundle; immutableEphemeralAccessRequestresources resolve, activate, expire, and revoke one grant without touching the durablePostgresPolicy.approval.mode: Requiredis only a real approval boundary under admission enforcement — approving and otherwise managing a request are the same write toephemeralaccessrequests/status, so RBAC alone cannot separate them. Deploy the CI-tested Kyverno profile ink8s/security/, or front the API with a trusted broker, before relying on it: securing ephemeral access sets out the three trust postures. (#158) - A generated Helm chart reference documenting every value. Previously 14 of the 21 chart values were undocumented, including
serviceAccount.annotations(required for GKE Workload Identity) and theEPHEMERAL_ACCESS_MAXIMUM_DURATION/EPHEMERAL_ACCESS_MAX_PENDING_TTLceilings. Generated by helm-docs fromvalues.yaml; CI fails if it drifts. - Approving a plan that can never execute is now reported. A policy in
spec.mode: plannever consultsspec.approval, so annotating its plan is accepted and then does nothing — indistinguishable from a stalled operator. The policy now reports anApprovalIgnoredcondition and a warning Event, pointing atmode: applywithapproval: manual, which is the combination that gates an apply. - Namespace-scoped operator deployments. The chart value
operator.watchNamespacesetsWATCH_NAMESPACE, which scopes every operator watch and conflict-detection list to one namespace, and switches the chart fromClusterRole/ClusterRoleBindingto a namespacedRole/RoleBinding. Unset, the operator remains cluster-scoped as before. (#162)
Deprecated
- Omitting
spec.approvalon aPostgresPolicy. Behaviour is unchanged — the value is still inferred fromspec.mode(apply→auto,plan→manual) — but the inference hides whether a human gates SQL execution behind an unrelated field, andspec.modeitself defaults toapply. Policies relying on it report anApprovalUnsetcondition and incrementpgroles.deprecated.approval_unset. Migration: write down the value you already get. A future release will reject the omission. (#73)
Removed
PostgresPolicystatus fieldsplanned_sql,planned_sql_truncated, andlast_reconcile_time. Superseded byPostgresPolicyPlanin 0.5.0, but still written on every reconcile with pending changes. Migration: read plan SQL from the plan the policy points at —kubectl get pgplan $(kubectl get pgr <policy> -o jsonpath='{.status.current_plan_ref.name}') -o jsonpath='{.status.sqlInline}'— falling back to the gzipped ConfigMap instatus.sqlRef, or a truncatedstatus.sqlInlinefor plans too large for either. Replacelast_reconcile_timewithstatus.last_successful_reconcile_time. (#73)OperatorContext::new, in thepgroles-operatorcrate. It could not supply the shared request index the reconcilers now read, so a context built through it produced lookups that no watch ever fed. Migration: useOperatorContext::new_with_runtime_config, passing theRequestIndexfed by the request controller's watch and the optional watch namespace. (#162)
Fixed
- The operator no longer holds PostgreSQL connections open against every database it manages. Connection pools are cached for the operator's lifetime and inherited sqlx's 10-minute idle timeout, which never elapsed against the 5-minute default requeue interval — each reconcile re-touched the pool first, and sqlx's FIFO idle queue spread those touches across every pooled connection. A pool that once peaked at N concurrent connections therefore occupied N backends indefinitely, per database, whether or not anything was reconciling. Pools now drain to zero between reconciles.
What's Changed
- Add Kubernetes-native ephemeral PostgreSQL access by @hardbyte in #160
- feat!: remove deprecated PostgresPolicy status fields by @hardbyte in #164
- docs: operator-first documentation audit and restructure by @hardbyte in #167
- ci: validate doc manifests against generated CRD schemas, document chart values by @hardbyte in #168
- Scale ephemeral access reconciliation by @hardbyte in #165
- chore: prepare the 0.9.0 release by @hardbyte in #169
Full Changelog: v0.8.0...v0.9.0
v0.8.0
Fixed
- Policy names longer than 63 characters no longer break plan creation, lookup, or cleanup. Label values cap at 63 bytes while resource names allow 253, and the operator conflated the two rules. Thanks @aarons-afk for the report and original fix (#146, #152).
- Plans and plan-SQL ConfigMaps are matched by controller-owner UID, not by a truncated label. Two policies sharing a 63-byte name prefix could previously cross-approve and cross-delete each other's plans (#152).
- Schema owner transfers no longer strip the incoming owner's privileges when a stale explicit grant exists (#140).
Added
-
Role and profile
configdefaults, managed withALTER ROLE ... SET/RESETand diffed againstpg_roles.rolconfig(#132, #134).The headline use case is zero-downtime password rotation — both login roles
SET ROLEto a shared owner at connect time, so objects created under either credential stay accessible after a rotation:roles: - name: combined - name: blue login: true config: { role: combined } - name: green login: true config: { role: combined } memberships: - role: combined members: [{ name: blue }, { name: green }]
Also covers general role-level defaults (
search_path,statement_timeout, dot-qualified custom settings likeapp.tenant), with{schema}/{profile}substitution onprofiles[].config. Config values must be quoted strings:statement_timeout: "30000", not `30000 (so a manifest means the same thing to the CLI and the API server).See zero-downtime-password-rotation.yaml and the manifest reference.
-
Column-level grant detection.
diffandapplynow warn aboutGRANT SELECT (col) ...in managed schemas — previously a silent audit hole in authoritative mode. Detection only; the grants are still not managed. -
New docs: [executor privileges](https://hardbyte.github.io/pgroles/docs/executor-privileges/) and [limitations](https://hardbyte.github.io/pgroles/docs/limitations/).
Changed
- PostgreSQL support is documented as 16, 17, and 18 — the versions CI tests. PG 14–15 paths remain but are best-effort and untested.
- Dependencies: OpenTelemetry 0.31 → 0.32, plus
cmov,quinn-proto,next,js-yaml(#154).
What's Changed
- ci: bump remaining Node 20 actions to Node 24 runtimes by @hardbyte in #129
- feat: manage role-level config defaults (ALTER ROLE ... SET) by @hardbyte in #134
- docs: executor privileges guide, limitations page, accurate PG version claims by @hardbyte in #136
- test: property-based convergence harness for the diff engine by @hardbyte in #137
- feat: detect and warn about column-level grants in privilege-managed schemas by @hardbyte in #138
- feat: profile-level config defaults with {schema}/{profile} placeholders by @hardbyte in #139
- fix: single-pass convergence for schema owner transfer with stale owner grants by @hardbyte in #141
- docs: add pgroles agent skills by @hardbyte in #142
- fix: valid Kubernetes identifiers from any policy name by @hardbyte in #152
- chore(deps): refresh Rust and docs dependencies by @hardbyte in #154
- chore(release): 0.8.0 by @hardbyte in #153
Full Changelog: v0.7.8...v0.8.0
v0.7.8
Highlights
v0.7.8 packages the new operator and CLI surfaces needed for cloud/provider-managed role workflows and on-demand reconciles.
Operator and CRD
- Roles can now be marked
external: trueinPostgresPolicy. External roles remain usable in grants, schema ownership, default privileges, and memberships, but pgroles does not create, alter, drop, password-manage, or prune memberships granted from those provider-managed roles. This is aimed at Cloud SQL IAM users and groups whose login attributes and provider memberships are owned outside pgroles. (#124) PostgresPolicyreconciles can now be requested immediately by changing thereconcile.pgroles.io/requestedAtannotation. The operator watches that annotation separately from spec generation and records successful handling instatus.lastHandledReconcileAt. (#118)- The Helm chart and committed CRDs include both the
externalrole marker and force-reconcile status/annotation support.
CLI and GitOps
pgroles reconcileannotates a KubernetesPostgresPolicyand can optionally wait until the operator records the request as handled. (#118)pgroles render-bundlecomposes a policy bundle into a deterministic flat manifest for CI/GitOps flows, with--check,--output, and--no-headersupport. (#122)
Docs and Examples
- Added a validated multi-service PostgreSQL roles example covering bootstrap roles, service-owned schemas, migrations, team memberships, and cross-service isolation. (#112)
- Refreshed the docs dependency stack with Next.js 16.2.7. (#110)
Validation
Release validation passed for the published tag, including CI, the GitHub Release workflow, and the Helm OCI chart release workflow.
Full Changelog: v0.7.7...v0.7.8
v0.7.7
What's Changed
- Add connection.params.setRole for per-pool SET ROLE by @hardbyte in #120
- Prepare v0.7.7 release by @hardbyte in #121
Full Changelog: v0.7.6...v0.7.7
v0.7.6
What's Changed
Fixed
- Wildcard
EXECUTEgrants now converge on schemas that contain PostgreSQL procedures. pgroles now renders schema-wideobject.type: functionwildcard grants asGRANT EXECUTE ON ALL ROUTINES IN SCHEMA ..., and renders specific function/procedure grant and revoke targets asROUTINE, keeping existing manifests backward-compatible while covering extension-installed procedures such as pg_partman routines. (#113, #114)
Full Changelog: v0.7.5...v0.7.6
v0.7.5
What's Changed
Added
- Native GKE Workload Identity support for Cloud SQL IAM database authentication in the operator.
connection.params.auth.type: gcp_workload_identitynow lets params-mode connections fetch short-lived login tokens from the GKE metadata server, optionally impersonate a target Google service account, and refresh cached pools before token expiry. Staticpassword/passwordSecretvalues are mutually exclusive with this auth mode, andsslModedefaults torequire. (#114, #115)
Changed
- The operator and manifest documentation now have clearer validation and navigation paths: a dedicated manifest reference, a tooling guide with schema-validation examples, and Cloud SQL docs covering both native Workload Identity auth and proxy-based connectivity. (#111, #115)
Full Changelog: v0.7.4...v0.7.5
v0.7.4
What's Changed
Full Changelog: v0.7.3...v0.7.4
v0.7.3
What's Changed
Full Changelog: v0.7.2...v0.7.3
v0.7.2
Added
- Operator OTLP metrics now expose database inspection cost. The operator records inspection phase durations, inspected object counts, wildcard inventory size, unsatisfied wildcard scope counts, and grantability query/object counts so large-schema deployments can spot catalog-query regressions.
Changed
- Wildcard diagnostics now avoid grantability catalog scans when current ACLs already satisfy the wildcard, and scope grantability checks to the unsatisfied wildcard schema/object-type pairs.
Fixed
- Unsatisfiable wildcard grants now fail with a clear diagnostic instead of re-planning forever. A wildcard such as
function name: "*"remains strict desired state: every matching object must either already have the requested privilege or be grantable by the executor. When a matching object is missing the privilege and the executor lacks the correspondingWITH GRANT OPTION, CLIdiff/plan/applynow stop withUnsatisfiableWildcardGrantinstead of printing or applying repeated wildcard SQL. The operator reportsReady=FalseandDegraded=Truewith the same reason, leaves no newPostgresPolicyPlanor SQL ConfigMap for that reconcile, and retries at the normal policy interval. (#105, #106)
What's Changed
- Fix unsatisfiable wildcard grant churn by @hardbyte in #106
- Prepare v0.7.2 release by @hardbyte in #107
Full Changelog: v0.7.1...v0.7.2