Skip to content

Allow role_pattern at the spec level (mirror default_owner shape) #97

@hardbyte

Description

@hardbyte

Problem

role_pattern is currently only accepted on individual SchemaBinding entries (manifest.rs:255), with a per-schema default of "{schema}-{profile}". There is no spec-level setting.

For policies with many schemas using the same convention this leads to one of:

  1. Verbose duplication — repeating role_pattern: '{schema}-{profile}' (or any other custom pattern) on every schema entry just to make the convention explicit and grep-able.
  2. Silent failure when the field is misplacedPolicyManifest doesn't deny_unknown_fields, so writing role_pattern: at the spec level (which intuitively reads as "naming convention for this whole policy") is silently ignored by serde. The operator falls back to the per-schema default. If the per-schema default happens to match the user's intended pattern, behaviour is correct by coincidence; if it doesn't, the rename silently fails and produces a policy that doesn't match expectations until a reviewer notices.

I've now seen two engineers in the same project independently reach for top-level role_pattern first. The API doesn't reject the misplaced field, so the divergence is only caught when grants don't line up with what the manifest reads.

Proposal

Mirror the existing default_owner / owner: shape: a higher-scope value with optional per-schema override.

pub struct PolicyManifest {
    // ...
    #[serde(default)]
    pub role_pattern: Option<String>,
}

pub struct SchemaBinding {
    pub name: String,
    pub profiles: Vec<String>,
    #[serde(default)]
    pub role_pattern: Option<String>,  // was: String with default
    #[serde(default)]
    pub owner: Option<String>,
}

Resolution at expansion (around manifest.rs:489):

let pattern = schema_binding
    .role_pattern
    .as_deref()
    .or(manifest.role_pattern.as_deref())
    .unwrap_or("{schema}-{profile}");

Validation (must contain {profile}) stays as-is and applies regardless of which level declared the value.

Why this matches existing precedent

default_owner is already declared at the spec level with SchemaBinding.owner as a per-schema override. role_pattern is the same kind of cross-cutting convention; not having higher-scope support is the surprise. Aligning these makes the manifest shape easier to predict, and gives future cross-cutting conventions an obvious home.

Final placement depends on the bundle/fragment direction

Once #91 / #92 land and PostgresPolicyBundle is the canonical home for cross-cutting bundle-level configuration, role_pattern belongs there alongside default_owner, profiles, and connection settings — fragments then only override per-schema. This issue is the v1alpha1 / single-policy version of that change; the same resolution rule (bundle → fragment → schema → hardcoded default) carries over.

Optional hardening (separate concern, maybe a major-bump candidate)

Add #[serde(deny_unknown_fields)] to PolicyManifest so future misplaced fields fail loud instead of silent. Useful in general; surfaces the kind of issue this proposal addresses but is also worth doing on its own.

Migration

Backwards compatible:

  • Existing policies with per-schema role_pattern keep working unchanged.
  • Existing policies relying on the per-schema default keep working unchanged (default still resolves to "{schema}-{profile}" when no level sets it).
  • New policies can omit per-schema role_pattern entries entirely if a single convention applies, or set higher-scope value + per-schema overrides for mixed conventions.

Effort

Small — manifest struct change, expansion-site update, two-or-three tests (existing expand_custom_role_pattern at manifest.rs:889 is a good template; add cases for higher-scope only, schema overrides higher-scope, neither set), CHANGELOG, and a doc update in docs/src/pages/docs/manifest-format.md to document role_pattern next to default_owner. No CRD schema change required beyond the regenerated types.

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