Skip to content

Support composed policies and scoped ownership in the operator #91

@hardbyte

Description

@hardbyte

Summary

Support multi-document policy composition and first-class scope ownership in the Kubernetes operator so pgroles can work cleanly in environments where platform/SRE teams and application teams share responsibility for PostgreSQL access control.

Today the operator reconciles a single PostgresPolicy into one core PolicyManifest. That works well for smaller setups, but it becomes awkward in larger environments where:

  • platform/SRE wants to own connection settings, shared profiles, schema ownership, and privileged group roles
  • application teams want to own their service roles and memberships
  • multiple teams need independent review and GitOps workflows without editing one shared object

The operator already has same-database conflict detection between separate policies, but it does not yet support composing multiple fragments into one desired policy with explicit in-policy ownership boundaries.

Goals

  • Support policy composition from multiple scoped inputs
  • Make ownership boundaries explicit and validated before inspect/diff/apply
  • Keep one reconcile loop, one lock, one plan, and one apply transaction per database target
  • Reuse the same core composition semantics in the CLI and operator
  • Preserve the current single-policy path for simpler users

Non-goals

  • Reading arbitrary files from Git repos or ConfigMaps at reconcile time
  • Letting independently reconciled fragments race on the same database
  • Adding implicit "last writer wins" merge semantics

Proposed model

Introduce composition in pgroles-core first, then expose it through the operator.

Core concepts:

  • PolicyBundle
    • shared profiles and auth provider metadata
    • list of policy fragments to compose
  • PolicyFragment
    • one team's scoped contribution
    • declares resources plus an explicit ownership scope
  • ComposedPolicy
    • the validated result of bundle + fragments
  • OwnershipIndex
    • exact ownership of generated and declared objects
  • ManagedScope
    • the set of database objects pgroles is allowed to inspect/diff/manage

For the operator API, the cleanest shape is:

  • PostgresPolicyBundle
    • connection settings
    • interval / mode / approval
    • shared profiles
    • selector or refs for fragments
  • PostgresPolicyFragment
    • team-owned policy content
    • explicit scope ownership

Only the bundle should reconcile. Fragments are inputs, not independently applying resources.

Scope ownership

Ownership should be explicit, not inferred after the fact.

Examples:

  • schema facets such as owner and bindings
  • named roles
  • membership edges
  • exact grant keys
  • exact default-privilege keys
  • retirements by role name

If a fragment defines content outside its declared scope, the composed policy should fail validation.
If two fragments claim the same managed key, the bundle should go Conflict before any SQL is planned or applied.

Operator reconciliation semantics

The intended reconcile flow becomes:

  1. Reconcile PostgresPolicyBundle
  2. Load matching PostgresPolicyFragment resources
  3. Compose bundle + fragments through pgroles-core
  4. Validate duplicate/conflicting claims and out-of-scope declarations
  5. Build one desired graph and one managed scope
  6. Inspect the live database only within that managed scope
  7. Diff and either apply or publish a plan

This keeps the existing lower-half operator behavior intact:

  • same per-database locking
  • same advisory locking
  • same PostgresPolicyPlan style plan lifecycle
  • same status / events / OTLP observability model

Backward compatibility

  • Keep PostgresPolicy as the current single-document path
  • Add composition as a new API path rather than changing existing semantics underneath users
  • Reuse the same core composition library from CLI and operator so behavior stays aligned

Suggested phases

  1. Add composition and ownership primitives to pgroles-core
  2. Add CLI support for validating/composing bundles and fragments
  3. Add PostgresPolicyBundle / PostgresPolicyFragment CRDs
  4. Reconcile only bundles, using fragments as inputs
  5. Reuse existing plan/apply/status machinery at bundle level
  6. Document migration from single-policy to bundle/fragment workflows

Acceptance criteria

  • Conflicting fragment ownership is rejected before database inspection
  • Out-of-scope fragment declarations are rejected during validation
  • Generated roles/grants/default privileges from schema/profile expansion participate in ownership checks
  • One bundle reconcile produces one composed plan and one apply transaction
  • Existing PostgresPolicy users remain unaffected

Open questions

  • Should fragments be selected by label selector, explicit refs, or both?
  • Should PostgresPolicyPlan stay bundle-scoped only, or also reference the fragment set digest used to compose it?
  • Do we want bundle-local shared profiles only, or separately reusable cluster-scoped profile libraries later?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions