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:
- 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.
- Silent failure when the field is misplaced —
PolicyManifest 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.
Problem
role_patternis currently only accepted on individualSchemaBindingentries (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:
role_pattern: '{schema}-{profile}'(or any other custom pattern) on every schema entry just to make the convention explicit and grep-able.PolicyManifestdoesn'tdeny_unknown_fields, so writingrole_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_patternfirst. 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.Resolution at expansion (around
manifest.rs:489):Validation (must contain
{profile}) stays as-is and applies regardless of which level declared the value.Why this matches existing precedent
default_owneris already declared at the spec level withSchemaBinding.owneras a per-schema override.role_patternis 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
PostgresPolicyBundleis the canonical home for cross-cutting bundle-level configuration,role_patternbelongs there alongsidedefault_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)]toPolicyManifestso 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:
role_patternkeep working unchanged."{schema}-{profile}"when no level sets it).role_patternentries 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_patternatmanifest.rs:889is a good template; add cases for higher-scope only, schema overrides higher-scope, neither set), CHANGELOG, and a doc update indocs/src/pages/docs/manifest-format.mdto documentrole_patternnext todefault_owner. No CRD schema change required beyond the regenerated types.