Skip to content

feat: profile-level config defaults with {schema}/{profile} placeholders - #139

Merged
hardbyte merged 2 commits into
mainfrom
claude/profile-config-defaults
Jul 14, 2026
Merged

feat: profile-level config defaults with {schema}/{profile} placeholders#139
hardbyte merged 2 commits into
mainfrom
claude/profile-config-defaults

Conversation

@hardbyte

@hardbyte hardbyte commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Fourth in the stacked draft series (#136#137#138 ← this); base is claude/column-grant-detection so this PR shows only its own diff. Merge the stack in order and this retargets as parents merge.

What

Profiles can now declare the same role-level config map (ALTER ROLE ... SET defaults) that roles gained in #134, applied to every generated role with {schema} and {profile} substituted in values only:

profiles:
  editor:
    config:
      search_path: "{schema}"
      statement_timeout: "30s"

schemas:
  - name: inventory
    profiles: [editor]

generates inventory-editor with search_path: inventory. A per-schema search_path default is declared once per profile instead of repeated on every generated role.

Design

  • Substitution happens during expansion, before list-GUC canonicalization and before the existing config validation loop — so parameter-name validation and the config.role → membership cross-check apply to generated roles exactly as to hand-written ones (tested both ways). Keys are never substituted; a {schema} key fails the existing parameter-name validation.
  • Suggest stays lossless: pgroles generate --suggest-profiles never clusters a config-carrying role into a profile (same disqualification mechanism as password/superuser), because collapsing config maps modulo placeholder substitution risks silently changing what gets applied. The suggest fuzz corpus (~1 in 6 generated roles now carries config) keeps this path exercised across seeds, not just in one hand-written test.
  • CRD ProfileSpec gains the field; both committed CRD copies regenerated; composition/render-bundle reuse the core Profile type so config flows through bundles automatically.

Adversarial review findings (fixed in d1f9936)

  • Confirmed pre-existing bug, widened by this feature: render-bundle's default-stripping matched role_pattern by leaf key name anywhere in the manifest tree, so a role/profile config parameter literally named role_pattern with the default-pattern value was silently stripped from rendered bundles. Stripping is now exact-path (schemas[i].role_pattern only), with a regression test; the render round-trip test now asserts config maps survive verbatim instead of only comparing role names.
  • Live-test cleanup helper dropped roles before schemas, which would fail via pg_shdepend (and silently leak) once a test grants schema privileges — order flipped.

Testing

  • Unit: placeholder substitution (single/combined/repeated/no-placeholder), invalid parameter names, unquoted-scalar rejection, config.role membership validation on generated roles (rejects and accepts), suggest round-trip preserving config, render-bundle regression.
  • Live (--include-ignored): new profile_config_placeholder_round_trips — manifest with schema-bound profile config → expand → diff (asserts substituted ALTER ROLE ... SET) → execute → re-inspect → empty plan.
  • Full workspace: 737 tests green; clippy clean; CRD drift clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_019TwqLzRSJR8WHpLvvVqkCg

Summary by CodeRabbit

  • New Features
    • Added profile-level role configuration defaults with {schema} and {profile} placeholder support.
    • Added documentation for executor privileges and product limitations.
  • Bug Fixes
    • Improved manifest handling so user-provided role configuration is preserved correctly.
  • Improvements
    • Column-level grants are now detected and reported as warnings without blocking reconciliation.
    • Suggested profiles keep configuration-bearing roles flat.
    • Clarified PostgreSQL compatibility: versions 16–18 are tested; 14–15 are best-effort.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@hardbyte, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 37adc9a5-28df-4a27-8a32-ecdf5c13cc91

📥 Commits

Reviewing files that changed from the base of the PR and between 71be823 and 6e79ce4.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • charts/pgroles-operator/crds/postgrespolicies.pgroles.io.yaml
  • crates/pgroles-cli/src/lib.rs
  • crates/pgroles-core/src/composition.rs
  • crates/pgroles-core/src/manifest.rs
  • crates/pgroles-core/src/model.rs
  • crates/pgroles-core/src/suggest.rs
  • crates/pgroles-core/tests/suggest_property.rs
  • crates/pgroles-inspect/tests/config_roundtrip.rs
  • crates/pgroles-operator/src/crd.rs
  • docs/src/pages/docs/manifest-reference.md
  • docs/src/pages/docs/profiles.md
  • k8s/crd.yaml
📝 Walkthrough

Walkthrough

Adds profile-level role configuration defaults with placeholder expansion, preserves configuration-bearing roles during suggestion and rendering, introduces advisory column-grant detection, updates reconciliation handling, and adds property-based and live validation with supporting schema and documentation changes.

Changes

Profile configuration defaults

Layer / File(s) Summary
Profile config contract and expansion
crates/pgroles-core/..., crates/pgroles-operator/..., charts/..., k8s/..., crates/pgroles-cli/src/lib.rs
Profiles accept configuration maps, substitute {schema} and {profile} in generated role values, validate parameter names, and preserve unrelated role_pattern values.
Suggestion and round-trip validation
crates/pgroles-core/src/suggest.rs, crates/pgroles-core/src/model.rs, crates/pgroles-inspect/tests/config_roundtrip.rs
Roles with configuration remain flat in suggested manifests and retain configuration through expansion and database round trips.
Documentation and compatibility updates
docs/src/pages/docs/*, docs/src/components/Layout.jsx, CHANGELOG.md, ROADMAP.md
Documents profile configuration, pooler behavior, executor privileges, limitations, and PostgreSQL 14–18 compatibility details.

Column-level grant diagnostics

Layer / File(s) Summary
Diagnostic model and ACL inspection
crates/pgroles-inspect/src/lib.rs, crates/pgroles-inspect/src/privileges.rs
Column ACLs are queried, grouped into deterministic diagnostics, capped with example columns, and rendered separately from blocking wildcard diagnostics.
CLI and operator reconciliation
crates/pgroles-cli/src/main.rs, crates/pgroles-operator/src/reconciler.rs
Wildcard diagnostics remain fatal, while column-level diagnostics are emitted as warnings and reconciliation continues.

Diff engine validation

Layer / File(s) Summary
Convergent diff property harness
crates/pgroles-core/tests/diff_property.rs
Property tests validate convergence, idempotence, determinism, and additive-mode restrictions using generated role graphs and an in-test change interpreter.
Live diff convergence validation
crates/pgroles-inspect/tests/diff_property_live.rs
Ignored live tests generate seeded PostgreSQL drift, apply plans, verify convergence and idempotence, and compare live results with an interpreter.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant Manifest
  participant ProfileExpansion
  participant GeneratedRole
  participant Database
  Manifest->>ProfileExpansion: define profile config
  ProfileExpansion->>GeneratedRole: substitute schema/profile values
  GeneratedRole->>Database: apply ALTER ROLE defaults
  Database-->>GeneratedRole: persisted role configuration
Loading
sequenceDiagram
  participant Inspector
  participant PostgreSQL
  participant CLIOrOperator
  Inspector->>PostgreSQL: inspect column ACLs
  PostgreSQL-->>Inspector: column-level grant rows
  Inspector->>CLIOrOperator: diagnostics
  CLIOrOperator-->>CLIOrOperator: fail on wildcard issues or warn on column grants
Loading

Poem

A rabbit hopped through config bright,
With schema paths tucked just right.
Column grants raised a gentle cheer,
While wildcard blockers stayed severe.
Tests danced on graphs beneath the moon.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: profile-level config defaults with placeholder substitution.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hardbyte hardbyte changed the title Add profile config defaults and column-level grant detection feat: profile-level config defaults with {schema}/{profile} placeholders Jul 13, 2026
@hardbyte
hardbyte changed the base branch from main to claude/column-grant-detection July 13, 2026 05:40
@hardbyte
hardbyte marked this pull request as draft July 13, 2026 05:40

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d1f993655f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

/// Values without either placeholder are returned unchanged.
fn substitute_placeholders(value: &str, schema: &str, profile: &str) -> String {
value
.replace("{schema}", schema)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Quote substituted list-GUC placeholders

When a profile config value is a list-quoted GUC such as search_path, replacing {schema} as raw text breaks schemas whose names contain list separators. For example, with schema.name: "tenant, archive" and search_path: "{schema}", this expands to tenant, archive; later RoleState::from_definition/render_set_config split list GUCs on commas/whitespace, so pgroles applies two path entries (tenant and archive) instead of the single quoted schema it just created. The substitution needs to preserve placeholder values as one list element for list-GUC parameters.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
crates/pgroles-inspect/src/lib.rs (2)

941-1035: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a direct test for blocking_message() with column-only diagnostics.

Existing tests cover is_empty and Display, but none assert that blocking_message() returns None when only column_level_grants is populated (no wildcard diagnostics). That's the exact behavior the CLI and operator rely on to avoid blocking reconciliation on advisory-only findings — worth a direct, explicit test given how critical that gating method is.

✅ Suggested test
#[test]
fn blocking_message_is_none_for_column_level_grants_only() {
    let mut diagnostics = InspectionDiagnostics::default();
    diagnostics
        .column_level_grants
        .push(sample_column_level_grant("analytics", &["secret"]));

    assert!(diagnostics.blocking_message().is_none());
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/pgroles-inspect/src/lib.rs` around lines 941 - 1035, Add a focused
test beside the existing InspectionDiagnostics tests for blocking_message(),
populate only column_level_grants using sample_column_level_grant, and assert
that blocking_message() returns None without any wildcard diagnostics.

626-640: 🚀 Performance & Scalability | 🔵 Trivial

Column-level grant counts aren't tracked in InspectionStats.

The new phase records timing via stats.record_phase(...) but never sets a count field on stats the way the roles/grants/schemas phases do (e.g., stats.grants = graph.grants.len()). Since ctx.observability.record_inspection(&inspection.stats) is the path that likely feeds operator metrics/dashboards, this advisory category is only visible via a conditional debug! log — invisible to anyone monitoring InspectionStats-derived metrics in production. Consider adding a count field so operators can alert on column-level grant trends the same way they can for other inspected categories.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/pgroles-inspect/src/lib.rs` around lines 626 - 640, Set the
column-level grant count on InspectionStats after fetch_column_level_grants
returns, alongside the existing stats.record_phase call. Use
diagnostics.column_level_grants.len() and the established stats field for this
category, ensuring ctx.observability.record_inspection receives the count for
metrics and dashboards.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/src/pages/docs/limitations.md`:
- Line 36: In the limitations documentation paragraph, update the phrase
“outside of pgroles” to “outside pgroles” while leaving the surrounding
explanation unchanged.

---

Nitpick comments:
In `@crates/pgroles-inspect/src/lib.rs`:
- Around line 941-1035: Add a focused test beside the existing
InspectionDiagnostics tests for blocking_message(), populate only
column_level_grants using sample_column_level_grant, and assert that
blocking_message() returns None without any wildcard diagnostics.
- Around line 626-640: Set the column-level grant count on InspectionStats after
fetch_column_level_grants returns, alongside the existing stats.record_phase
call. Use diagnostics.column_level_grants.len() and the established stats field
for this category, ensuring ctx.observability.record_inspection receives the
count for metrics and dashboards.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b3d87284-c50f-437c-82b8-8ab50b758ef3

📥 Commits

Reviewing files that changed from the base of the PR and between c2c53c0 and d1f9936.

📒 Files selected for processing (26)
  • CHANGELOG.md
  • ROADMAP.md
  • charts/pgroles-operator/crds/postgrespolicies.pgroles.io.yaml
  • crates/pgroles-cli/src/lib.rs
  • crates/pgroles-cli/src/main.rs
  • crates/pgroles-core/src/composition.rs
  • crates/pgroles-core/src/manifest.rs
  • crates/pgroles-core/src/model.rs
  • crates/pgroles-core/src/suggest.rs
  • crates/pgroles-core/tests/diff_property.rs
  • crates/pgroles-core/tests/suggest_property.rs
  • crates/pgroles-inspect/src/lib.rs
  • crates/pgroles-inspect/src/privileges.rs
  • crates/pgroles-inspect/tests/config_roundtrip.rs
  • crates/pgroles-operator/src/crd.rs
  • crates/pgroles-operator/src/reconciler.rs
  • docs/src/components/Layout.jsx
  • docs/src/pages/docs/architecture.md
  • docs/src/pages/docs/executor-privileges.md
  • docs/src/pages/docs/installation.md
  • docs/src/pages/docs/limitations.md
  • docs/src/pages/docs/manifest-reference.md
  • docs/src/pages/docs/memberships.md
  • docs/src/pages/docs/profiles.md
  • docs/src/pages/docs/quick-start.md
  • k8s/crd.yaml

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

🧹 Nitpick comments (2)
crates/pgroles-inspect/src/lib.rs (2)

941-1035: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a direct test for blocking_message() with column-only diagnostics.

Existing tests cover is_empty and Display, but none assert that blocking_message() returns None when only column_level_grants is populated (no wildcard diagnostics). That's the exact behavior the CLI and operator rely on to avoid blocking reconciliation on advisory-only findings — worth a direct, explicit test given how critical that gating method is.

✅ Suggested test
#[test]
fn blocking_message_is_none_for_column_level_grants_only() {
    let mut diagnostics = InspectionDiagnostics::default();
    diagnostics
        .column_level_grants
        .push(sample_column_level_grant("analytics", &["secret"]));

    assert!(diagnostics.blocking_message().is_none());
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/pgroles-inspect/src/lib.rs` around lines 941 - 1035, Add a focused
test beside the existing InspectionDiagnostics tests for blocking_message(),
populate only column_level_grants using sample_column_level_grant, and assert
that blocking_message() returns None without any wildcard diagnostics.

626-640: 🚀 Performance & Scalability | 🔵 Trivial

Column-level grant counts aren't tracked in InspectionStats.

The new phase records timing via stats.record_phase(...) but never sets a count field on stats the way the roles/grants/schemas phases do (e.g., stats.grants = graph.grants.len()). Since ctx.observability.record_inspection(&inspection.stats) is the path that likely feeds operator metrics/dashboards, this advisory category is only visible via a conditional debug! log — invisible to anyone monitoring InspectionStats-derived metrics in production. Consider adding a count field so operators can alert on column-level grant trends the same way they can for other inspected categories.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/pgroles-inspect/src/lib.rs` around lines 626 - 640, Set the
column-level grant count on InspectionStats after fetch_column_level_grants
returns, alongside the existing stats.record_phase call. Use
diagnostics.column_level_grants.len() and the established stats field for this
category, ensuring ctx.observability.record_inspection receives the count for
metrics and dashboards.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/src/pages/docs/limitations.md`:
- Line 36: In the limitations documentation paragraph, update the phrase
“outside of pgroles” to “outside pgroles” while leaving the surrounding
explanation unchanged.

---

Nitpick comments:
In `@crates/pgroles-inspect/src/lib.rs`:
- Around line 941-1035: Add a focused test beside the existing
InspectionDiagnostics tests for blocking_message(), populate only
column_level_grants using sample_column_level_grant, and assert that
blocking_message() returns None without any wildcard diagnostics.
- Around line 626-640: Set the column-level grant count on InspectionStats after
fetch_column_level_grants returns, alongside the existing stats.record_phase
call. Use diagnostics.column_level_grants.len() and the established stats field
for this category, ensuring ctx.observability.record_inspection receives the
count for metrics and dashboards.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b3d87284-c50f-437c-82b8-8ab50b758ef3

📥 Commits

Reviewing files that changed from the base of the PR and between c2c53c0 and d1f9936.

📒 Files selected for processing (26)
  • CHANGELOG.md
  • ROADMAP.md
  • charts/pgroles-operator/crds/postgrespolicies.pgroles.io.yaml
  • crates/pgroles-cli/src/lib.rs
  • crates/pgroles-cli/src/main.rs
  • crates/pgroles-core/src/composition.rs
  • crates/pgroles-core/src/manifest.rs
  • crates/pgroles-core/src/model.rs
  • crates/pgroles-core/src/suggest.rs
  • crates/pgroles-core/tests/diff_property.rs
  • crates/pgroles-core/tests/suggest_property.rs
  • crates/pgroles-inspect/src/lib.rs
  • crates/pgroles-inspect/src/privileges.rs
  • crates/pgroles-inspect/tests/config_roundtrip.rs
  • crates/pgroles-operator/src/crd.rs
  • crates/pgroles-operator/src/reconciler.rs
  • docs/src/components/Layout.jsx
  • docs/src/pages/docs/architecture.md
  • docs/src/pages/docs/executor-privileges.md
  • docs/src/pages/docs/installation.md
  • docs/src/pages/docs/limitations.md
  • docs/src/pages/docs/manifest-reference.md
  • docs/src/pages/docs/memberships.md
  • docs/src/pages/docs/profiles.md
  • docs/src/pages/docs/quick-start.md
  • k8s/crd.yaml
🛑 Comments failed to post (1)
docs/src/pages/docs/limitations.md (1)

36-36: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Minor wording redundancy.

"outside of pgroles" → "outside pgroles" ("of" is redundant here).

✏️ Suggested wording fix
-PostgreSQL does not expose password hashes for comparison, so pgroles cannot detect when a password has been changed directly in the database outside of pgroles.
+PostgreSQL does not expose password hashes for comparison, so pgroles cannot detect when a password has been changed directly in the database outside pgroles.
Based on static analysis: `[style] This phrase is redundant. Consider using "outside".`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

PostgreSQL does not expose password hashes for comparison, so pgroles cannot detect when a password has been changed directly in the database outside pgroles. Passwords are re-applied on every `apply` from the configured source (CLI environment variable or operator Secret), and password-only changes are excluded from `diff --exit-code` drift detection since they always appear in the plan. See [passwords and drift detection](/docs/manifest-reference#roles) in the manifest reference.
🧰 Tools
🪛 LanguageTool

[style] ~36-~36: This phrase is redundant. Consider using “outside”.
Context: ...s been changed directly in the database outside of pgroles. Passwords are re-applied on ev...

(OUTSIDE_OF)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/src/pages/docs/limitations.md` at line 36, In the limitations
documentation paragraph, update the phrase “outside of pgroles” to “outside
pgroles” while leaving the surrounding explanation unchanged.

Source: Linters/SAST tools

@hardbyte
hardbyte force-pushed the claude/profile-config-defaults branch from d1f9936 to 71be823 Compare July 13, 2026 08:52
@hardbyte
hardbyte force-pushed the claude/column-grant-detection branch 2 times, most recently from 6fb3f48 to bd50540 Compare July 14, 2026 09:27
@hardbyte
hardbyte changed the base branch from claude/column-grant-detection to main July 14, 2026 10:09
@hardbyte
hardbyte marked this pull request as ready for review July 14, 2026 10:09
claude added 2 commits July 14, 2026 10:10
Profiles can now declare the same ALTER ROLE ... SET config map as
roles, applied to every generated role with {schema} and {profile}
substituted in values (keys stay literal parameter names — a
placeholder key fails the existing parameter-name validation). A
per-schema search_path default can therefore be declared once:

  profiles:
    editor:
      config:
        search_path: "{schema}"

Generated roles flow through the existing RoleDefinition.config
pipeline, so parameter-name validation, the config.role membership
cross-check, list-GUC canonicalization, and diffing all apply
unchanged. pgroles generate --suggest-profiles keeps config-carrying
roles flat rather than clustering them into profiles, preserving the
expand(suggest(M)) == expand(M) round-trip contract.

Operator CRD ProfileSpec gains the same field; CRDs regenerated.
Live round-trip test covers placeholder substitution end to end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TwqLzRSJR8WHpLvvVqkCg
Two finder agents reviewed the profile-config diff; fixes:

- render-bundle's default-stripping matched `role_pattern` by leaf
  key name only, so a role/profile config parameter literally named
  role_pattern with the default-pattern value was silently stripped
  from rendered bundles (pre-existing since role config landed, but
  widened by profile config). Stripping is now exact-path
  (schemas[i].role_pattern only), with a regression test, and the
  render round-trip test now asserts config maps survive verbatim
  rather than only comparing role names.
- The suggest fuzz corpus now generates config-carrying roles
  (~1 in 6) so the suggester's config-disqualification path is
  exercised across seeds, not just by one hand-written unit test.
- RoleAndSchemaCleanup in the live round-trip tests drops schemas
  before roles — DROP ROLE fails via pg_shdepend while the role still
  holds grants on a surviving schema, so the old order would leak
  roles if a future test grants schema privileges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TwqLzRSJR8WHpLvvVqkCg
@hardbyte
hardbyte force-pushed the claude/profile-config-defaults branch from 71be823 to 6e79ce4 Compare July 14, 2026 10:15

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/pgroles-inspect/tests/diff_property_live.rs`:
- Around line 128-168: Update SeedCleanup::drop so a failed PgPool::connect is
handled without panicking: log the cleanup connection error and return from the
cleanup future instead of calling expect. Preserve SeedCleanup::run for
successful connections and ensure cleanup remains non-panicking during
unwinding.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c685c5aa-8e8d-44b4-a03f-ece67465ec47

📥 Commits

Reviewing files that changed from the base of the PR and between d1f9936 and 71be823.

📒 Files selected for processing (20)
  • CHANGELOG.md
  • charts/pgroles-operator/crds/postgrespolicies.pgroles.io.yaml
  • crates/pgroles-cli/src/lib.rs
  • crates/pgroles-cli/src/main.rs
  • crates/pgroles-core/src/composition.rs
  • crates/pgroles-core/src/manifest.rs
  • crates/pgroles-core/src/model.rs
  • crates/pgroles-core/src/suggest.rs
  • crates/pgroles-core/tests/diff_property.rs
  • crates/pgroles-core/tests/suggest_property.rs
  • crates/pgroles-inspect/src/lib.rs
  • crates/pgroles-inspect/src/privileges.rs
  • crates/pgroles-inspect/tests/config_roundtrip.rs
  • crates/pgroles-inspect/tests/diff_property_live.rs
  • crates/pgroles-operator/src/crd.rs
  • crates/pgroles-operator/src/reconciler.rs
  • docs/src/pages/docs/limitations.md
  • docs/src/pages/docs/manifest-reference.md
  • docs/src/pages/docs/profiles.md
  • k8s/crd.yaml
🚧 Files skipped from review as they are similar to previous changes (18)
  • crates/pgroles-core/src/composition.rs
  • k8s/crd.yaml
  • charts/pgroles-operator/crds/postgrespolicies.pgroles.io.yaml
  • docs/src/pages/docs/limitations.md
  • crates/pgroles-cli/src/main.rs
  • crates/pgroles-core/tests/suggest_property.rs
  • docs/src/pages/docs/manifest-reference.md
  • crates/pgroles-core/src/model.rs
  • docs/src/pages/docs/profiles.md
  • crates/pgroles-core/src/suggest.rs
  • crates/pgroles-inspect/tests/config_roundtrip.rs
  • crates/pgroles-operator/src/crd.rs
  • crates/pgroles-operator/src/reconciler.rs
  • crates/pgroles-core/src/manifest.rs
  • crates/pgroles-core/tests/diff_property.rs
  • crates/pgroles-inspect/src/privileges.rs
  • crates/pgroles-inspect/src/lib.rs
  • CHANGELOG.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/pgroles-inspect/tests/diff_property_live.rs`:
- Around line 128-168: Update SeedCleanup::drop so a failed PgPool::connect is
handled without panicking: log the cleanup connection error and return from the
cleanup future instead of calling expect. Preserve SeedCleanup::run for
successful connections and ensure cleanup remains non-panicking during
unwinding.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c685c5aa-8e8d-44b4-a03f-ece67465ec47

📥 Commits

Reviewing files that changed from the base of the PR and between d1f9936 and 71be823.

📒 Files selected for processing (20)
  • CHANGELOG.md
  • charts/pgroles-operator/crds/postgrespolicies.pgroles.io.yaml
  • crates/pgroles-cli/src/lib.rs
  • crates/pgroles-cli/src/main.rs
  • crates/pgroles-core/src/composition.rs
  • crates/pgroles-core/src/manifest.rs
  • crates/pgroles-core/src/model.rs
  • crates/pgroles-core/src/suggest.rs
  • crates/pgroles-core/tests/diff_property.rs
  • crates/pgroles-core/tests/suggest_property.rs
  • crates/pgroles-inspect/src/lib.rs
  • crates/pgroles-inspect/src/privileges.rs
  • crates/pgroles-inspect/tests/config_roundtrip.rs
  • crates/pgroles-inspect/tests/diff_property_live.rs
  • crates/pgroles-operator/src/crd.rs
  • crates/pgroles-operator/src/reconciler.rs
  • docs/src/pages/docs/limitations.md
  • docs/src/pages/docs/manifest-reference.md
  • docs/src/pages/docs/profiles.md
  • k8s/crd.yaml
🚧 Files skipped from review as they are similar to previous changes (18)
  • crates/pgroles-core/src/composition.rs
  • k8s/crd.yaml
  • charts/pgroles-operator/crds/postgrespolicies.pgroles.io.yaml
  • docs/src/pages/docs/limitations.md
  • crates/pgroles-cli/src/main.rs
  • crates/pgroles-core/tests/suggest_property.rs
  • docs/src/pages/docs/manifest-reference.md
  • crates/pgroles-core/src/model.rs
  • docs/src/pages/docs/profiles.md
  • crates/pgroles-core/src/suggest.rs
  • crates/pgroles-inspect/tests/config_roundtrip.rs
  • crates/pgroles-operator/src/crd.rs
  • crates/pgroles-operator/src/reconciler.rs
  • crates/pgroles-core/src/manifest.rs
  • crates/pgroles-core/tests/diff_property.rs
  • crates/pgroles-inspect/src/privileges.rs
  • crates/pgroles-inspect/src/lib.rs
  • CHANGELOG.md
🛑 Comments failed to post (1)
crates/pgroles-inspect/tests/diff_property_live.rs (1)

128-168: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Reconnect failure inside Drop::drop panics during unwinding — risks aborting the process and losing the real assertion failure.

SeedCleanup::drop calls .expect("failed to connect for cleanup") on PgPool::connect. When run_seed panics on a genuine property violation (the entire point of this suite), cleanup is dropped during unwinding; if that reconnect also fails (transient network blip, connection-limit exhaustion from the many pools already opened this run, etc.), the second panic inside a destructor while already unwinding aborts the whole test process, discarding the original diagnostic message that pinpoints the seed and mismatch.

🛡️ Proposed fix: log instead of panicking on reconnect failure in the cleanup path
 impl Drop for SeedCleanup {
     fn drop(&mut self) {
         let roles = self.roles.clone();
         let schemas = self.schemas.clone();
         with_runtime(async move {
-            let pool = PgPool::connect(&database_url())
-                .await
-                .expect("failed to connect for cleanup");
-            Self::run(&pool, &roles, &schemas).await;
+            match PgPool::connect(&database_url()).await {
+                Ok(pool) => Self::run(&pool, &roles, &schemas).await,
+                Err(error) => {
+                    eprintln!("SeedCleanup: failed to connect for cleanup: {error}");
+                }
+            }
         });
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

/// Drop-guard that removes every generated schema and role, even on panic.
/// Runs in sync context (its own runtime + connection), like `RoleCleanup` in
/// `config_roundtrip.rs`. Errors are ignored — objects may not exist.
struct SeedCleanup {
    roles: Vec<String>,
    schemas: Vec<String>,
}

impl SeedCleanup {
    async fn run(pool: &PgPool, roles: &[String], schemas: &[String]) {
        for schema in schemas {
            let _ = pool
                .execute(format!("DROP SCHEMA IF EXISTS {} CASCADE;", quote_ident(schema)).as_str())
                .await;
        }
        for role in roles {
            // DROP OWNED clears grants and default-privilege entries that
            // would otherwise block DROP ROLE. It fails when the role does
            // not exist — ignored like everything else here.
            let _ = pool
                .execute(format!("DROP OWNED BY {};", quote_ident(role)).as_str())
                .await;
            let _ = pool
                .execute(format!("DROP ROLE IF EXISTS {};", quote_ident(role)).as_str())
                .await;
        }
    }
}

impl Drop for SeedCleanup {
    fn drop(&mut self) {
        let roles = self.roles.clone();
        let schemas = self.schemas.clone();
        with_runtime(async move {
            match PgPool::connect(&database_url()).await {
                Ok(pool) => Self::run(&pool, &roles, &schemas).await,
                Err(error) => {
                    eprintln!("SeedCleanup: failed to connect for cleanup: {error}");
                }
            }
        });
    }
}
🧰 Tools
🪛 ast-grep (0.44.1)

[error] 138-139: SQL query is built with a format! macro that interpolates dynamic values directly into the query string. Passing this to a query/execute sink (e.g. sqlx::query, diesel::sql_query, conn.execute) allows SQL injection. Use parameterized queries with bind placeholders (``/? and `.bind(...)`, or `diesel`'s `.bind::<Type, _>(value)`) instead of string interpolation.
Context: pool
.execute(format!("DROP SCHEMA IF EXISTS {} CASCADE;", quote_ident(schema)).as_str())
Note: [CWE-89] Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection').

(sql-injection-format-rust)


[error] 146-147: SQL query is built with a format! macro that interpolates dynamic values directly into the query string. Passing this to a query/execute sink (e.g. sqlx::query, diesel::sql_query, conn.execute) allows SQL injection. Use parameterized queries with bind placeholders (``/? and `.bind(...)`, or `diesel`'s `.bind::<Type, _>(value)`) instead of string interpolation.
Context: pool
.execute(format!("DROP OWNED BY {};", quote_ident(role)).as_str())
Note: [CWE-89] Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection').

(sql-injection-format-rust)


[error] 149-150: SQL query is built with a format! macro that interpolates dynamic values directly into the query string. Passing this to a query/execute sink (e.g. sqlx::query, diesel::sql_query, conn.execute) allows SQL injection. Use parameterized queries with bind placeholders (``/? and `.bind(...)`, or `diesel`'s `.bind::<Type, _>(value)`) instead of string interpolation.
Context: pool
.execute(format!("DROP ROLE IF EXISTS {};", quote_ident(role)).as_str())
Note: [CWE-89] Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection').

(sql-injection-format-rust)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/pgroles-inspect/tests/diff_property_live.rs` around lines 128 - 168,
Update SeedCleanup::drop so a failed PgPool::connect is handled without
panicking: log the cleanup connection error and return from the cleanup future
instead of calling expect. Preserve SeedCleanup::run for successful connections
and ensure cleanup remains non-panicking during unwinding.

@hardbyte
hardbyte merged commit 97b389d into main Jul 14, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants