Skip to content

fix(policy): resolve this.relation.field against @@allow model, unnest array columns in subqueries#2734

Open
hechang27-sprt wants to merge 4 commits into
zenstackhq:devfrom
hechang27-sprt:fix/this-authscope-relation-resolution
Open

fix(policy): resolve this.relation.field against @@allow model, unnest array columns in subqueries#2734
hechang27-sprt wants to merge 4 commits into
zenstackhq:devfrom
hechang27-sprt:fix/this-authscope-relation-resolution

Conversation

@hechang27-sprt

@hechang27-sprt hechang27-sprt commented Jun 26, 2026

Copy link
Copy Markdown

Fixes #2733.

Problem

Two bugs in the policy plugin expression transformer:

  1. this.relation.field resolves against the wrong model inside auth().collection?[...] predicates. The expression transformer uses context.thisType to resolve the field definition but passes restContext (with modelOrType still pointing to the collection model) to transformRelationAccess. This causes Field "X" not found in model "<collection>" crashes.

  2. x in this.relation.arrayField generates broken SQL on PostgreSQL. The expression compiles to = ANY((SELECT arrayCol FROM ...)) where the subquery returns array-typed rows (integer[]). PostgreSQL ANY(subquery) requires scalar rows, producing operator does not exist: integer = integer[].

Fix

Fix 1: Resolve this.relation.field against the @@allow model

Pass modelOrType: context.thisType and alias: context.thisAlias to transformRelationAccess when the receiver is this, so relation fields and table aliases resolve against the model bearing the policy. Also fixed getFieldDefFromFieldRef to resolve multi-segment this.relation.field chains (needed for the array-field detection in Fix 2).

Fix 2: Cross-database scalar IN relation.arrayField

Single entry point buildArrayInExists in _binary, with provider-specific handling:

Provider Approach Generated SQL
PostgreSQL unnest() in subquery column + = ANY(subquery) scalar = ANY(SELECT unnest("col") FROM "t" WHERE <join>)
SQLite Nested EXISTS with json_each EXISTS (SELECT 1 FROM "t" WHERE <join> AND EXISTS (SELECT 1 FROM json_each("t"."col") WHERE "value" = scalar))
MySQL Nested EXISTS with JSON_TABLE EXISTS (SELECT 1 FROM "t" WHERE <join> AND EXISTS (SELECT 1 FROM JSON_TABLE(...) AS jt WHERE jt."value" = scalar))

The SQLite/MySQL EXISTS rebuild preserves the original subquery FROM clause, so many-to-many joins are kept intact.

Verification

Two new PG-specific tests added to auth-access.test.ts:

Test Without fix With fix
this.author.level in auth().permissions?[p, ...] Field "author" not found in model "Permission" ✓ passes
this.id in this.group.visibleDocIds operator does not exist: integer = integer[] ✓ passes

All existing policy tests continue to pass on PostgreSQL, SQLite, and MySQL (no regressions).

Summary by CodeRabbit

  • New Features

    • Expanded policy rules to support in checks against array-based subqueries across databases.
    • Improved handling of this-based relation and collection references in authorization conditions.
  • Bug Fixes

    • Fixed access rules that compare related fields, including nested relation chains.
    • Corrected evaluation of collection predicates so permissions and group-based visibility work reliably together.
    • Improved support for array membership checks in policy expressions for PostgreSQL, SQLite, and MySQL.

…t array columns in subqueries

Fix zenstackhq#1: In _member() isThis branch, pass modelOrType: context.thisType
to transformRelationAccess() so that this.relation.field correctly
resolves relation fields against the @@Allow model (thisType) rather
than the collection predicate's model (modelOrType).

Fix zenstackhq#2: In _member() when the innermost field of a subquery selection
is an array type AND the provider is PostgreSQL, wrap the column with
unnest() so that ANY(subquery) receives scalar rows instead of array
rows. Other providers (SQLite, MySQL) leave the column as-is since
they store arrays differently (JSON) and would need different handling.
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Policy expression resolution now handles nested this and binding-rooted predicate access, and array-subquery in checks compile through provider-specific SQL. E2e tests cover the updated authorization paths.

Changes

Policy expression resolution and array IN

Layer / File(s) Summary
Predicate and member resolution
packages/plugins/policy/src/expression-transformer.ts
Relation operand normalization, collection predicate scoping, member-type resolution, nested this traversal, reverse array member handling, and field-definition lookup were updated together.
Array in subquery compilation
packages/plugins/policy/src/expression-transformer.ts
Array-typed subquery in expressions now compile through buildArrayInExists, which preserves subquery structure and emits provider-specific SQL for PostgreSQL, SQLite, and MySQL.
Policy e2e coverage
tests/e2e/orm/policy/auth-access.test.ts
Three e2e cases cover relation-field access inside auth().permissions?, in checks against array fields, and this-rooted collection predicates under auth bindings.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • zenstackhq/zenstack#2723: Also changes policy in compilation in packages/plugins/policy/src/expression-transformer.ts, so the operator transformation paths overlap.

Poem

🐰 I hopped through this with careful glee,
and found each relation where it should be.
Array EXISTS blinked in SQL light,
PostgreSQL, SQLite, and MySQL felt right.
The burrow hums with policy cheer tonight.

🚥 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 summarizes the main policy fix and the related array-subquery SQL change.
Linked Issues check ✅ Passed The changes resolve this.relation.field inside auth collection predicates and add tests covering the reported runtime crash.
Out of Scope Changes check ✅ Passed The transformer updates and tests all support the linked policy-resolution and array-membership fixes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":500,"request":{"method":"PATCH","url":"https://api.github.com/repos/zenstackhq/zenstack/issues/comments/4808645116","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- review_stack_entry_start -->\n\n[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/zenstackhq/zenstack/pull/2734?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)\n\n<!-- review_stack_entry_end -->\n<!-- This is an auto-generated comment: review in progress by coderabbit.ai -->\n\n> [!NOTE]\n> Currently processing new changes in this PR. This may take a few minutes, please wait...\n> \n> <details>\n> <summary>⚙️ Run configuration</summary>\n> \n> **Configuration used**: Path: .coderabbit.yaml\n> \n> **Review profile**: CHILL\n> \n> **Plan**: Pro\n> \n> **Run ID**: `6db5667e-2c09-4e16-9f54-cff855f6d775`\n> \n> </details>\n> \n> <details>\n> <summary>📥 Commits</summary>\n> \n> Reviewing files that changed from the base of the PR and between 8a317258b5e47ceabcf4a424b4453d77628f35e3 and 19658414e4c37a31a8affe6f015a7c668af5057a.\n> \n> </details>\n> \n> <details>\n> <summary>📒 Files selected for processing (2)</summary>\n> \n> * `packages/plugins/policy/src/expression-transformer.ts`\n> * `tests/e2e/orm/policy/auth-access.test.ts`\n> \n> </details>\n> \n> ```ascii\n>  ______________________________________________________________________________________________________________________________________________________________________________________________________\n> < Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it. - Brian Kernighan >\n>  ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n>   \\\n>    \\   (\\__/)\n>        (•ㅅ•)\n>        /   づ\n> ```\n\n<!-- end of auto-generated comment: review in progress by coderabbit.ai -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing Touches</summary>\n\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=zenstackhq/zenstack&utm_content=2734)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n\n<sub>Comment `@coderabbitai help` to get the list of available commands.</sub>\n\n<!-- tips_end -->"},"request":{"retryCount":3,"signal":{},"retries":3,"retryAfter":16}}}

@hechang27-sprt hechang27-sprt force-pushed the fix/this-authscope-relation-resolution branch from 1965841 to cfbc2ce Compare June 27, 2026 01:32

@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: 3

🤖 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 `@packages/plugins/policy/src/expression-transformer.ts`:
- Around line 1073-1082: The buildArrayInExists path is reconstructing a new
select from only the first table and blindly reading subquery.where, which
breaks many-to-many relation queries that carry correlation in joins and may
have no where clause. Update buildArrayInExists to work from the existing
SelectQueryNode shape, preserving any joins and other query parts instead of
using froms[0] alone, and append the containment predicate to the incoming
subquery rather than dereferencing subquery.where! or rebuilding the query from
scratch.
- Around line 712-715: The first `this` relation hop in
`expression-transformer.ts` is still carrying `restContext.alias`, which can
resolve to the wrong table alias in nested policy predicates. Update the
`transformRelationAccess` call in this branch so that when `modelOrType` is
switched back to `context.thisType`, the alias also uses `context.thisAlias` for
consistency with the one-segment `this.foo` handling, keeping the outer
`@@allow` model reference stable.
- Around line 257-262: The array-subquery branch in expression-transformer is
returning early before the enum scalar cast logic runs, so enum comparisons skip
the same normalization used by the non-subquery any(...) path. Update the
handling in ExpressionTransformer around buildArrayInExists so the right-hand
expression is still passed through the existing enum scalar casting logic before
routing to buildArrayInExists, keeping the cast behavior consistent for
array-column subqueries and regular any(...) comparisons.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4f6aeb4a-8549-4cdc-82d9-5aa6b985dfd8

📥 Commits

Reviewing files that changed from the base of the PR and between 805a6b8 and cfbc2ce.

📒 Files selected for processing (2)
  • packages/plugins/policy/src/expression-transformer.ts
  • tests/e2e/orm/policy/auth-access.test.ts

Comment thread packages/plugins/policy/src/expression-transformer.ts
Comment thread packages/plugins/policy/src/expression-transformer.ts
Comment thread packages/plugins/policy/src/expression-transformer.ts Outdated
@hechang27-sprt hechang27-sprt force-pushed the fix/this-authscope-relation-resolution branch 2 times, most recently from da34156 to 34d86b9 Compare June 27, 2026 02:07

@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.

♻️ Duplicate comments (1)
packages/plugins/policy/src/expression-transformer.ts (1)

1083-1128: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve joins when rebuilding the non-PostgreSQL EXISTS.

Line 1127 creates a fresh selectFrom(tableName), so any subquery.joins from relation access—especially many-to-many joins from transformManyToManyRelationAccess—are dropped. That can make SQLite/MySQL array membership checks uncorrelated or incorrect even though subquery.where is merged.

Verification script
#!/bin/bash
# Inspect relation subquery shapes and generated array-in helper call sites.
ast-grep outline packages/plugins/policy/src/expression-transformer.ts --match ExpressionTransformer --view expanded

rg -n -C4 'buildArrayInExists|transformManyToManyRelationAccess|subquery\.joins|selectFrom\(tableName\)' packages/plugins/policy/src/expression-transformer.ts
🤖 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 `@packages/plugins/policy/src/expression-transformer.ts` around lines 1083 -
1128, The non-PostgreSQL EXISTS rebuild in expression-transformer drops relation
joins by creating a new selectFrom(tableName), which breaks many-to-many and
other correlated array checks. Update the logic around buildArrayInExists so it
reuses the existing subquery shape, including subquery.joins, when constructing
the final EXISTS for SQLite/MySQL, while still merging subquery.where with the
array membership predicate. Keep the original table/alias handling from
transformManyToManyRelationAccess and preserve all join conditions instead of
starting a fresh root query.
🤖 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.

Duplicate comments:
In `@packages/plugins/policy/src/expression-transformer.ts`:
- Around line 1083-1128: The non-PostgreSQL EXISTS rebuild in
expression-transformer drops relation joins by creating a new
selectFrom(tableName), which breaks many-to-many and other correlated array
checks. Update the logic around buildArrayInExists so it reuses the existing
subquery shape, including subquery.joins, when constructing the final EXISTS for
SQLite/MySQL, while still merging subquery.where with the array membership
predicate. Keep the original table/alias handling from
transformManyToManyRelationAccess and preserve all join conditions instead of
starting a fresh root query.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e26234ba-921d-477d-8c75-4d3c1efeac5c

📥 Commits

Reviewing files that changed from the base of the PR and between da34156 and 34d86b9.

📒 Files selected for processing (2)
  • packages/plugins/policy/src/expression-transformer.ts
  • tests/e2e/orm/policy/auth-access.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/e2e/orm/policy/auth-access.test.ts

…-db array-in EXISTS

Fixes zenstackhq#2733.

Fix 1: `_member` isThis branch now passes `modelOrType: context.thisType`
to `transformRelationAccess`, so `this.relation.field` resolves against
the @@Allow model instead of the collection predicate model.

Fix 2: `this.scalarField in this.relation.arrayField` now generates a
cross-database EXISTS subquery instead of `= ANY(subquery)`. Each
provider uses its native array operator:
- PostgreSQL: EXISTS (SELECT 1 FROM t WHERE join AND scalar = ANY(t.col))
- SQLite:    EXISTS (... AND EXISTS (SELECT 1 FROM json_each(t.col) WHERE value = scalar))
- MySQL:     EXISTS (... AND EXISTS (SELECT 1 FROM JSON_TABLE(t.col, ...) AS jt WHERE jt.value = scalar))

Also fixes `getFieldDefFromFieldRef` to resolve multi-segment
`this.relation.field` chains, needed to detect array fields in the
`in`-operator RHS.

Tests: two new PG tests in auth-access.test.ts, verified to fail
without the fix and pass with it.
@hechang27-sprt hechang27-sprt force-pushed the fix/this-authscope-relation-resolution branch from 34d86b9 to 9e78de7 Compare June 27, 2026 02:24
@hechang27-sprt

hechang27-sprt commented Jun 27, 2026

Copy link
Copy Markdown
Author

Pushed commit 29a60bb to branch fix/this-authscope-relation-resolution.

What changed:

  • preserve this-rooted collection predicates on the @@Allow model when they appear inside auth-binding predicates
  • normalize relation comparisons in those value-backed paths
  • add a focused regression in tests/e2e/orm/policy/auth-access.test.ts (Fix feat: zmodel schema and type-checking #3)

Verification:

  • A/B tested the new ZenStack regression directly in tests/e2e/orm/policy/auth-access.test.ts
  • with patch: PASS
  • stashing only packages/plugins/policy/src/expression-transformer.ts, rebuilding @zenstackhq/plugin-policy, same test: FAIL with "Field "authScope" not found in model "RoleAssignment""
  • restoring the patch and rebuilding: PASS

So the new fix is necessary, and those two broader-suite failures are not reproduced by simply removing this patch alone.

@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

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/plugins/policy/src/expression-transformer.ts (1)

330-338: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Compare every relation id field, or reject composite-id relations.

requireIdFields(...) can return multiple fields, but this normalizes relation equality to only idFields[0]. With composite ids, policy relation comparisons can match on one key and over-authorize records. Prefer expanding equality across all id fields, or fail closed for composite-id relation comparisons.

Safer minimal guard
 const idFields = QueryUtils.requireIdFields(this.schema, leftRelDef.type);
+if (idFields.length !== 1) {
+    throw createUnsupportedError('Relation comparison with composite id fields is not supported');
+}
 normalizedLeft = this.makeOrAppendMember(normalizedLeft, idFields[0]!);

Apply the same guard on the right-hand relation branch.

🤖 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 `@packages/plugins/policy/src/expression-transformer.ts` around lines 330 -
338, The relation-equality normalization in expression-transformer currently
uses only idFields[0] from QueryUtils.requireIdFields, which is unsafe for
composite IDs. Update the handling around isRelationField and makeOrAppendMember
so relation comparisons either expand across every ID field returned by
requireIdFields or reject composite-id relations outright, and apply the same
fail-closed guard to the right-hand relation branch as well.
♻️ Duplicate comments (1)
packages/plugins/policy/src/expression-transformer.ts (1)

1147-1153: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Preserve the original subquery joins in the EXISTS rewrite.

The rebuilt SelectQueryNode only keeps from, where, and selections, so many-to-many relation subqueries lose their innerJoin correlation. On SQLite/MySQL this can turn an array-membership policy into an uncorrelated global EXISTS.

Preserve the incoming query shape
 return UnaryOperationNode.create(
     {
-        kind: 'SelectQueryNode',
-        from: subquery.from,
+        ...subquery,
         where: WhereNode.create(combinedWhere),
         selections: [SelectionNode.create(AliasNode.create(ValueNode.createImmediate(1), IdentifierNode.create('_')))],
     } as SelectQueryNode,
     OperatorNode.create('exists'),
 );
#!/bin/bash
# Verify that many-to-many relation access creates joins and buildArrayInExists preserves them.
sed -n '916,936p' packages/plugins/policy/src/expression-transformer.ts
sed -n '1141,1155p' packages/plugins/policy/src/expression-transformer.ts
rg -n "joins\\??:|interface SelectQueryNode|type SelectQueryNode" .
🤖 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 `@packages/plugins/policy/src/expression-transformer.ts` around lines 1147 -
1153, The EXISTS rewrite in the subquery builder is dropping query shape by
reconstructing SelectQueryNode with only from, where, and selections. Update the
rewrite in buildArrayInExists/expression-transformer to preserve the incoming
subquery properties, especially innerJoin (and any other relevant query fields),
while only replacing the where and selections needed for EXISTS. Use the
existing subquery object as the base so many-to-many relation correlation
survives on SQLite/MySQL.
🧹 Nitpick comments (2)
tests/e2e/orm/policy/auth-access.test.ts (2)

535-568: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise the SQLite/MySQL array-subquery branches.

This test uses PostgreSQL only, but the implementation adds SQLite/MySQL-specific json_each / JSON_TABLE SQL. Add provider coverage here so those branches don’t regress silently.

🤖 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 `@tests/e2e/orm/policy/auth-access.test.ts` around lines 535 - 568, The current
auth-access test only covers PostgreSQL for the this.relation.arrayField in
operator case, so the SQLite/MySQL-specific json_each and JSON_TABLE branches
are untested. Update the createPolicyTestClient setup in auth-access.test.ts and
the related test case to run under SQLite and MySQL as well (using the same
model/auth().permissions?[p, this.id in p.allowedDocIds] path), so these
provider-specific SQL branches are exercised and guarded against regressions.

675-680: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a denial assertion for the auth-scope predicate.

The new test proves the intended ancestor assignment grants access, but it would still pass if the policy overgranted all authenticated users. Add a no-assignment or unrelated-scope reader expectation that resolves null.

🤖 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 `@tests/e2e/orm/policy/auth-access.test.ts` around lines 675 - 680, Add a
denial check in the auth-access test so it verifies the auth-scope predicate is
actually enforced, not just that an ancestor assignment works. In the same
reader setup around db.$setAuth and reader.doc.findUnique, add a second
expectation using a reader with no assignments or an unrelated scope that
resolves null. Keep the existing granted-access assertion for the intended
ancestor case and ensure the new negative case uses the same findUnique path to
validate the policy behavior.
🤖 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.

Outside diff comments:
In `@packages/plugins/policy/src/expression-transformer.ts`:
- Around line 330-338: The relation-equality normalization in
expression-transformer currently uses only idFields[0] from
QueryUtils.requireIdFields, which is unsafe for composite IDs. Update the
handling around isRelationField and makeOrAppendMember so relation comparisons
either expand across every ID field returned by requireIdFields or reject
composite-id relations outright, and apply the same fail-closed guard to the
right-hand relation branch as well.

---

Duplicate comments:
In `@packages/plugins/policy/src/expression-transformer.ts`:
- Around line 1147-1153: The EXISTS rewrite in the subquery builder is dropping
query shape by reconstructing SelectQueryNode with only from, where, and
selections. Update the rewrite in buildArrayInExists/expression-transformer to
preserve the incoming subquery properties, especially innerJoin (and any other
relevant query fields), while only replacing the where and selections needed for
EXISTS. Use the existing subquery object as the base so many-to-many relation
correlation survives on SQLite/MySQL.

---

Nitpick comments:
In `@tests/e2e/orm/policy/auth-access.test.ts`:
- Around line 535-568: The current auth-access test only covers PostgreSQL for
the this.relation.arrayField in operator case, so the SQLite/MySQL-specific
json_each and JSON_TABLE branches are untested. Update the
createPolicyTestClient setup in auth-access.test.ts and the related test case to
run under SQLite and MySQL as well (using the same model/auth().permissions?[p,
this.id in p.allowedDocIds] path), so these provider-specific SQL branches are
exercised and guarded against regressions.
- Around line 675-680: Add a denial check in the auth-access test so it verifies
the auth-scope predicate is actually enforced, not just that an ancestor
assignment works. In the same reader setup around db.$setAuth and
reader.doc.findUnique, add a second expectation using a reader with no
assignments or an unrelated scope that resolves null. Keep the existing
granted-access assertion for the intended ancestor case and ensure the new
negative case uses the same findUnique path to validate the policy behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9d644c5f-6492-449a-9fe6-0055a687d37f

📥 Commits

Reviewing files that changed from the base of the PR and between 9e78de7 and 29a60bb.

📒 Files selected for processing (2)
  • packages/plugins/policy/src/expression-transformer.ts
  • tests/e2e/orm/policy/auth-access.test.ts

@hechang27-sprt

Copy link
Copy Markdown
Author

Addressed the latest CodeRabbit finding in commit 9992d72.

Change:

  • buildArrayInExists no longer reconstructs a fresh query from froms[0]
  • it now reuses the incoming SelectQueryNode shape and only replaces:
    • where with the original predicate conjoined with the array containment check
    • selections with a constant projection for EXISTS

This preserves joins/correlation from relation access subqueries, including the many-to-many path CodeRabbit called out, and also avoids assuming subquery.where already exists.

Verification:

  • pnpm -C devenv/zenstack-fork --filter @zenstackhq/plugin-policy build
  • TEST_PG_HOST=localhost TEST_PG_PORT=5432 TEST_PG_USER=postgres TEST_PG_PASSWORD=123456 pnpm -C devenv/zenstack-fork/tests/e2e exec vitest run orm/policy/auth-access.test.ts -t "Fix #2" --config vitest.config.ts
  • TEST_PG_HOST=localhost TEST_PG_PORT=5432 TEST_PG_USER=postgres TEST_PG_PASSWORD=123456 pnpm -C devenv/zenstack-fork/tests/e2e exec vitest run orm/policy/auth-access.test.ts -t "Fix #3" --config vitest.config.ts

I tried to add a dedicated m2m regression for this.m2mRelation.arrayField, but the ZModel resolver rejects that direct member chain today (Could not resolve reference to MemberAccessTarget). So the code fix is in, but there is not yet a focused schema-level regression for that exact syntax shape.

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.

Bug: this.relationField resolves against collection model inside auth().collection?[...] predicates

1 participant