Skip to content

Releases: thepartly/pgroles

v0.10.0-alpha.1

v0.10.0-alpha.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 21 Aug 06:28
Immutable release. Only release title and notes can be modified.
161067b

Added

  • PostgresPolicyCandidate: propose and review policy content without touching the live policy. A candidate points at an existing PostgresPolicy and carries only proposed content — roles, grants, memberships. Everything about execution (interval, mode, approval and, unless spec.target overrides 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

    (#182, #173)

  • 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 with Ready=False, reason=BlockedByActivePolicy; an active ephemeral grant that touches a candidate's effects sends its plan back for fresh review with OverlayOverlap. (#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 in mode: observe (PromotionNotExecuted). The candidate docs give the kubectl and CI recipes for the whole flow. (#182, #173)

  • pgroles candidate covers the review side of the candidate workflow, so proposing and reading a change no longer means hand-rolling jsonpath. create files a candidate from the ordinary manifest your PR promotes — validated locally through the same path as pgroles validate, so a proposal the API server would reject on size bounds fails on your machine with the same field-level message, and created with generateName so two people filing against one policy never collide. list shows every candidate for a policy with its phase, digest, plan and condition reasons; status expands one of them down to its plan's decision, who made it, whether it is still current, and what promotion had to say; diff prints 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 stays kubectl-shaped on purpose: it is a status write gated by admission so decidedBy records 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. Set spec.connection.requirePhysicalIdentity: true to 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_privileges entries accept scope: {type: global} beside the existing schema: shorthand, emitting ALTER DEFAULT PRIVILEGES FOR ROLE ... with no IN SCHEMA clause. 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 explicit pg_default_acl row still compares against what PostgreSQL will apply. Owner self-entries are excluded, because every ALTER DEFAULT PRIVILEGES materializes 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 in diff output, and in a bundle only the document owning the owner role may declare them. See default privileges.

  • ensure: absent and a typed PUBLIC grantee. PostgreSQL grants EXECUTE on every function to PUBLIC without writing an ACL entry, so no combination of positive grants could take it away — a SECURITY DEFINER routine stayed callable by every role. Grant entries and default-privilege entries now accept ensure: absent, which revokes a privilege where it is held, and role: PUBLIC addresses the pseudo-role (rendered unquoted, never as the identifier "PUBLIC"). Inspection reports PUBLIC's effective privileges, synthesizing acldefault(...) 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 a present PUBLIC rule does not revoke it — switch the rule to ensure: absent. additive ignores absence assertions with a warning, since it never revokes; adopt and authoritative apply them. A profile is an additive template, so a profile grant or default privilege that sets ensure: absent is rejected by name instead of expanding to its opposite. Preflight warns on diff and 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. Superseded is generated churn — every replan supersedes its predecessor — so on an active policy it filled the pool and deleted the Applied plans, 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 now Applied 25 (never fewer than 30 days' worth, hard ceiling 200), Failed and Rejected 10 shared, Superseded 3; Pending, Approved and Applying are 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=true still 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_SUPERSEDED on the operator environment, replacing the max_plans parameter that nothing could ever set — and an invalid value refuses operator startup with the variable named. Deliberately not a PostgresPolicy field: retention caps object growth in the cluster, and the per-object need is what the keep label is for. The Applied bounds measure — and order — by status.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 an Applied plan is held to the Applied bounds instead of the flat terminal-candidate bound, and pgroles.io/keep=true on 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 tagged scope ({"type": "schema", "schema": "app"} or {"type": "global"}) in place of the bare schema string, which could not express a global rule. Migration: read scope.schema where you read schema, and handle scope.type == "global" entries having no schema at all.

  • diff --format json carries the same tagged scope on default-privilege changes. Unlike bundle output it has no schema_version field to bump, so nothing announces the change in the payload itself. Migration: the same one as above — read scope.schema where you read schema, and handle scope.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.name is required for type: database, and inspecti...

Read more

0.9.0

Choose a tag to compare

@hardbyte hardbyte released this 14 Aug 21:17
Immutable release. Only release title and notes can be modified.
6a04607

Added

  • Bounded, request-driven PostgreSQL memberships in Kubernetes. EphemeralAccessPolicy defines a requestable bundle; immutable EphemeralAccessRequest resources resolve, activate, expire, and revoke one grant without touching the durable PostgresPolicy. approval.mode: Required is only a real approval boundary under admission enforcement — approving and otherwise managing a request are the same write to ephemeralaccessrequests/status, so RBAC alone cannot separate them. Deploy the CI-tested Kyverno profile in k8s/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 the EPHEMERAL_ACCESS_MAXIMUM_DURATION / EPHEMERAL_ACCESS_MAX_PENDING_TTL ceilings. Generated by helm-docs from values.yaml; CI fails if it drifts.
  • Approving a plan that can never execute is now reported. A policy in spec.mode: plan never consults spec.approval, so annotating its plan is accepted and then does nothing — indistinguishable from a stalled operator. The policy now reports an ApprovalIgnored condition and a warning Event, pointing at mode: apply with approval: manual, which is the combination that gates an apply.
  • Namespace-scoped operator deployments. The chart value operator.watchNamespace sets WATCH_NAMESPACE, which scopes every operator watch and conflict-detection list to one namespace, and switches the chart from ClusterRole/ClusterRoleBinding to a namespaced Role/RoleBinding. Unset, the operator remains cluster-scoped as before. (#162)

Deprecated

  • Omitting spec.approval on a PostgresPolicy. Behaviour is unchanged — the value is still inferred from spec.mode (applyauto, planmanual) — but the inference hides whether a human gates SQL execution behind an unrelated field, and spec.mode itself defaults to apply. Policies relying on it report an ApprovalUnset condition and increment pgroles.deprecated.approval_unset. Migration: write down the value you already get. A future release will reject the omission. (#73)

Removed

  • PostgresPolicy status fields planned_sql, planned_sql_truncated, and last_reconcile_time. Superseded by PostgresPolicyPlan in 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 in status.sqlRef, or a truncated status.sqlInline for plans too large for either. Replace last_reconcile_time with status.last_successful_reconcile_time. (#73)
  • OperatorContext::new, in the pgroles-operator crate. 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: use OperatorContext::new_with_runtime_config, passing the RequestIndex fed 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

Choose a tag to compare

@hardbyte hardbyte released this 03 Aug 01:21
Immutable release. Only release title and notes can be modified.
7942ac1

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 config defaults, managed with ALTER ROLE ... SET / RESET and diffed against pg_roles.rolconfig (#132, #134).

    The headline use case is zero-downtime password rotation — both login roles SET ROLE to 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 like app.tenant), with {schema}/{profile} substitution on profiles[].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. diff and apply now warn about GRANT 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

Choose a tag to compare

@github-actions github-actions released this 04 Jun 01:37

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: true in PostgresPolicy. 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)
  • PostgresPolicy reconciles can now be requested immediately by changing the reconcile.pgroles.io/requestedAt annotation. The operator watches that annotation separately from spec generation and records successful handling in status.lastHandledReconcileAt. (#118)
  • The Helm chart and committed CRDs include both the external role marker and force-reconcile status/annotation support.

CLI and GitOps

  • pgroles reconcile annotates a Kubernetes PostgresPolicy and can optionally wait until the operator records the request as handled. (#118)
  • pgroles render-bundle composes a policy bundle into a deterministic flat manifest for CI/GitOps flows, with --check, --output, and --no-header support. (#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

Choose a tag to compare

@github-actions github-actions released this 18 May 06:20

What's Changed

Full Changelog: v0.7.6...v0.7.7

v0.7.6

Choose a tag to compare

@hardbyte hardbyte released this 14 May 05:38
594e680

What's Changed

Fixed

  • Wildcard EXECUTE grants now converge on schemas that contain PostgreSQL procedures. pgroles now renders schema-wide object.type: function wildcard grants as GRANT EXECUTE ON ALL ROUTINES IN SCHEMA ..., and renders specific function/procedure grant and revoke targets as ROUTINE, 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

Choose a tag to compare

@github-actions github-actions released this 14 May 04:20

What's Changed

Added

  • Native GKE Workload Identity support for Cloud SQL IAM database authentication in the operator. connection.params.auth.type: gcp_workload_identity now 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. Static password / passwordSecret values are mutually exclusive with this auth mode, and sslMode defaults to require. (#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

Choose a tag to compare

@github-actions github-actions released this 12 May 06:00

What's Changed

  • fix(inspect): cast inventory object_name columns to text in UNION by @hardbyte in #109

Full Changelog: v0.7.3...v0.7.4

v0.7.3

Choose a tag to compare

@github-actions github-actions released this 12 May 05:18

What's Changed

Full Changelog: v0.7.2...v0.7.3

v0.7.2

Choose a tag to compare

@hardbyte hardbyte released this 08 May 08:38
3a031f2

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 corresponding WITH GRANT OPTION, CLI diff/plan/apply now stop with UnsatisfiableWildcardGrant instead of printing or applying repeated wildcard SQL. The operator reports Ready=False and Degraded=True with the same reason, leaves no new PostgresPolicyPlan or SQL ConfigMap for that reconcile, and retries at the normal policy interval. (#105, #106)

What's Changed

Full Changelog: v0.7.1...v0.7.2