Skip to content

fix(policy): compare before() enum fields as text on PostgreSQL - #2869

Merged
ymc9 merged 2 commits into
zenstackhq:devfrom
theluckystrike:fix/post-update-before-enum-pg
Oct 23, 2026
Merged

ymc9 merged 2 commits into
zenstackhq:devfrom
theluckystrike:fix/post-update-before-enum-pg

Conversation

@theluckystrike

@theluckystrike theluckystrike commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2727

What broke. On PostgreSQL with a native enum column, a post-update rule that compares before().field with the current field fails with operator does not exist: text <> "State" instead of evaluating the rule.

Root cause. The $before table for post-update checks is built by buildValuesTableSelect, and the PostgreSQL dialect casts every enum field there to text (packages/orm/src/client/crud/dialects/postgresql.ts:503-505, used at :657). The current row's column keeps its native enum type, and _binary in packages/plugins/policy/src/expression-transformer.ts:325-337 compares the two as they are. PostgreSQL has no operator between text and a user enum type.

Fix. In _binary, when either operand is a before() access to an enum field, both operands go through dialect.castText before buildComparison. The in branch already does the same for enum fields. Comparisons that don't involve before() enums are unchanged, so normal enum filters keep using the column as is.

How I tested it.

  • New regression test tests/regression/test/issue-2727.test.ts runs on PostgreSQL with usePrismaPush: true.
  • On dev it fails with operator does not exist: text <> "State". With the fix the denied transition is rejected by policy and the allowed one goes through.
  • I also tried the same rule with @map on the enum values and @@map on the enum. It passes with the fix (not added as a test).
  • tests/e2e orm/policy on PostgreSQL 16 and SQLite gives the same results with and without the change. A few tests fail locally on both (for example the now() tests on PostgreSQL), so that's my environment.
  • pnpm lint in packages/plugins/policy and tsc --noEmit in tests/regression are clean.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed policy checks for updates involving enum fields accessed through before(), so comparisons correctly distinguish allowed and rejected state changes.
    • Comparisons now behave consistently in rules that check the updated record directly or evaluate related records, preventing prohibited transitions from being accepted while preserving permitted updates.

The post-update `$before` VALUES table casts enum fields to text, so a
rule like `before().state != state` produced `text <> "State"`, which
PostgreSQL rejects for native enum columns. Cast both operands to text
when one side is a `before()` enum field.

Fixes zenstackhq#2727

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between ae35b2b and 06f699a.

📒 Files selected for processing: 2
  • packages/plugins/policy/src/expression-transformer.ts
  • tests/regression/test/issue-2727.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

📝 Walkthrough

The policy expression transformer detects comparisons involving a before() enum field and casts both operands to text. PostgreSQL regression tests check direct and related-record policy comparisons for rejected and accepted state transitions.

Changes: PostgreSQL enum policy comparisons

Layer / File(s) Summary
Transform and validate enum comparisons
packages/plugins/policy/src/expression-transformer.ts, tests/regression/test/issue-2727.test.ts
The transformer detects a before() enum field and casts both comparison operands to text. Regression tests check direct and related-record policy expressions. Updates to DONE are rejected, while updates to IN_PROGRESS succeed.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: ymc9

Merge Risk: ⚪ Minimal · up to 06f69

The change makes PostgreSQL post-update policy rules that compare a before() enum value with the current value evaluate without the operator error. No actionable merge-blocking risk was found, and regression tests cover both the rejected and the allowed transitions.

Security architecture risk: 🔵 Low · up to ae35b

The fix preserves policy enforcement and rollback for automatically managed transactions. Rollback remains unresolved when an application catches a rejected update inside an already-open transaction.

Retained concerns

  • Medium · security · inferred: Atomic denial is unresolved when a caller catches rejection inside an existing transaction. The mutation executes before the post-update check, and the inspected executor rolls back only transactions it starts. This PR changes affected native-enum comparisons from PostgreSQL type errors to application-level policy rejection. A caught rejection could therefore leave the denied update committable unless the surrounding transaction supplies an additional abort or rollback guarantee. The rollback dependency predates this PR; the concern is its exposure through the newly executable comparison.
Security review details

Security Blast Radius

  • inferred — The identified exposure is bounded to updates governed by affected before-enum post-update predicates, particularly PostgreSQL native enums. Exploitation of the unresolved recovery path would additionally require an application integration that continues an existing transaction after catching rejection. No direct tenant or infrastructure privilege expansion is established.

Security Findings and Attack Paths

  • inferred — A conditional attack path is an otherwise permitted update containing a policy-forbidden enum transition, followed by application handling that catches the post-update rejection and commits the existing transaction. The update precedes rejection, but whether the surrounding transaction prevents this outcome remains unverified. Automatically managed rollback blocks this path.

Trust Boundaries and Controls

  • observed — The changed code constructs SQL predicate nodes through existing dialect operations; it does not introduce a runtime policy parser or change the before-row join and policy-count controls.

Hardening Proposals

  • proposed — Establish the recovery contract for policy rejection caught inside an existing transaction: either undo the rejected mutation or prevent that transaction from committing. Validate the contract by checking persisted state after rejection and attempted continuation.
Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title names the main change: comparisons of before() enum fields are cast to text in PostgreSQL policy rules. The raw summary confirms ExpressionTransformer._binary now casts operands to text …
Linked Issues check Passed Issue #2727 requires PostgreSQL post-update policy comparisons between a native enum field's before() value and its current value to avoid the text-to-enum operator error. _binary detects a `befor…
Out of Scope Changes check Passed The changes are limited to enum comparison handling in packages/plugins/policy/src/expression-transformer.ts and regression tests in tests/regression/test/issue-2727.test.ts. The implementation an…
Docstring Coverage Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 2…
Full details: Title check

Explanation

The title names the main change: comparisons of before() enum fields are cast to text in PostgreSQL policy rules. The raw summary confirms ExpressionTransformer._binary now casts operands to text when either side is a before() enum field, and the regression test covers this path. The title is specific and short, and it describes the change from the developer's view.

Full details: Linked Issues check

Explanation

Issue #2727 requires PostgreSQL post-update policy comparisons between a native enum field's before() value and its current value to avoid the text-to-enum operator error. _binary detects a before() enum field on either operand and casts both operands with dialect.castText before buildComparison. The regression tests cover a direct post-update comparison and a collection-predicate comparison. Each test verifies policy rejection for the denied transition and success for the allowed transition.

Full details: Out of Scope Changes check

Explanation

The changes are limited to enum comparison handling in packages/plugins/policy/src/expression-transformer.ts and regression tests in tests/regression/test/issue-2727.test.ts. The implementation and both tests directly support issue #2727. No unrelated change is shown in the supplied change summary.

Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 2 files.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/plugins/policy/src/expression-transformer.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

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.

…n collection predicates

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@ymc9 ymc9 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hey @theluckystrike , thanks for making the fix. Changes look good overall. I've made a small additional fix to use context.thisType to check for before(). access, which always points to the model being updated.

@ymc9
ymc9 merged commit ee71982 into zenstackhq:dev Oct 23, 2026
9 checks passed
@theluckystrike

theluckystrike commented Oct 26, 2026 via email •

Copy link
Copy Markdown
Contributor Author

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.

before() function in post-update policy rules fails with PostgreSQL native enums — operator does not exist: text <> "EnumType"

2 participants