Skip to content

fix(nextly): a collection field rule reads the grants the caller arrived with - #1851

Closed
mobeenabdullah wants to merge 1 commit into
mainfrom
fix/a-field-rule-reads-the-callers-own-grants
Closed

mobeenabdullah wants to merge 1 commit into
mainfrom
fix/a-field-rule-reads-the-callers-own-grants

Conversation

@mobeenabdullah

Copy link
Copy Markdown
Collaborator

Split out of #1844, which is being reworked. This half is independent, measured, and has drawn no findings across four review rounds there.

A field rule was answered on the wrong caller's authority

A field access rule receives permissions and roles, and it is asking about the caller. An API key carries the grants stamped on the key, deliberately narrower than the database roles of whoever owns it, because that is the point of scoping a key. None of the collection write paths passed that scope to the field-level pass, so the rules were answered from the key owner's roles instead.

Measured on main, sqlite. A field whose rule is ({ permissions }) => permissions.includes("write-secret"), written through an API key stamped with exactly that grant:

updateSucceeded = true
liveSecret      = "before"   ← "after" was dropped, silently

The key holds the grant the rule asks for, the write is stripped anyway, and the call reports success. It reads as a permission bug in both directions: a correctly scoped key cannot write its own field, and a key owned by a privileged user is judged on authority the key was never given.

Every collection write path passes params.authenticatedScope now, on create and on update alike. It was already available on all five call sites and already used elsewhere in the same methods; only the field pass was missed. The Singles path has done this since #1833.

This is not a promote-only problem. Four of the five sites are ordinary create and update.

Tests

Three, in field-rules-read-the-callers-own-grants.integration.test.ts:

  • a key stamped with the grant writes the field. Control against main: expected 'before' to be 'after'.
  • a key WITHOUT the grant is still refused, stripped as on any ordinary write. This is the control for the one above: passing the scope through must not mean passing everything. Passes against main too, which is what a guard should do.
  • the same on a create.

Verification

  • pnpm --filter nextly check-types clean.
  • 405 collections integration tests.

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@codex review

@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 11 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: d2107643-47ab-4ad0-8832-3bd48d068dc3

📥 Commits

Reviewing files that changed from the base of the PR and between 57cd97a and de2ceb1.

⛔ Files ignored due to path filters (1)
  • .changeset/a-field-rule-reads-the-callers-own-grants.md is excluded by !.changeset/**
📒 Files selected for processing (2)
  • packages/nextly/src/domains/collections/__tests__/field-rules-read-the-callers-own-grants.integration.test.ts
  • packages/nextly/src/domains/collections/services/collection-mutation-service.ts

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.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-13T10:36:23.337760Z de2ceb1 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added scope: core nextly type: docs Documentation only labels Sep 13, 2026

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

Copy link
Copy Markdown

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: de2ceb1df6

ℹ️ 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".

Comment on lines +69 to +72
const KEY_WITH_GRANT = {
actorType: "apiKey" as const,
permissions: [`update-${SLUG}`, `create-${SLUG}`, "write-secret"],
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Exercise the canonical API-key scope conversion

This fixture hand-assembles only stored permission slugs and then has the field rule look for write-secret. A real authenticated key scope carries grant rows, so ruleFacingPermissions converts that grant to the rule-facing spelling secret:write; the current fixture instead exercises the rowless/mock fallback and would stay green if the production conversion were bypassed. Build the scope with apiKeyScope and grant rows, and make the rule assert the resource:action spelling, so this security regression test represents an actual API-key request.

AGENTS.md reference: packages/nextly/AGENTS.md:L40-L45

Useful? React with 👍 / 👎.

data: finalData,
operation: "update",
user: params.user,
authenticatedScope: params.authenticatedScope,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Explain why the caller scope is forwarded

All five newly added authenticatedScope arguments are bare additions; the surrounding comments describe field stripping, overrides, and document IDs, but not why the API-key scope must accompany this pass or what authority error omission causes. Add a code-local what-and-why comment to these forwards, as required for every code change.

AGENTS.md reference: AGENTS.md:L288-L290

Useful? React with 👍 / 👎.

Comment on lines +137 to +140
const created = await h.createEntry(
{
collectionName: SLUG,
routeAuthorized: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Exercise the changed create path

This create case calls CollectionsHandler.createEntry, whose non-transactional create pipeline already forwarded authenticatedScope through all three field-access passes in the parent commit. It therefore passes both before and after this change and never reaches the newly edited transactional create pass at line 8552. Exercise createEntryInTransaction or the bulk-create path and confirm that test fails when the new forwarding is removed, so the added create assertion provides evidence for the changed code rather than an already-working path.

AGENTS.md reference: AGENTS.md:L189-L191

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown
Contributor

Whole-Repository Code Hygiene Summary

Full dead-code, duplication, and complexity report for the PR branch as it stands now. Playground is excluded. Quality gate enforcement on introduced issues is performed by the Changed files job.

🌿 Fallow

Warning

Review needed

⚠️ 73 code issues · ⚠️ 677 clone groups · ⚠️ 1035 health findings

See inline review comments for per-finding details.

Code issues (73)
Category Count
Unused files 2
Unused exports 5
Unused dependencies 19
Unused devDependencies 6
Unresolved imports 2
Unlisted dependencies 1
Circular dependencies 38
Duplication (677 groups · 28396 lines · 3.9%)
Locations Lines Tokens
schemas/_dialect-bundles/mysql.relations.ts:40-134
schemas/_dialect-bundles/postgres.relations.ts:40-134
schemas/_dialect-bundles/sqlite.relations.ts:40-134
95 593
cli/commands/db-sync-demote.ts:70-75
cli/commands/db-sync-promote.ts:38-43
cli/commands/dev-build.ts:100-105
cli/commands/dev-build.ts:179-184
cli/commands/dev-build.ts:299-304
cli/commands/dev-build.ts:411-416
cli/commands/dev-build.ts:552-557
cli/commands/dev-server.ts:575-580
cli/commands/dev-server.ts:840-845
cli/commands/dev-server.ts:1143-1148
cli/commands/migrate-field-groups.ts:110-115
6 70
entries/EntryList/EntryTableSkeleton.tsx:74-98
collection/components/CollectionTableSkeleton.tsx:94-118
field-group/components/FieldGroupTableSkeleton.tsx:90-114
plugins/components/PluginsTableSkeleton.tsx:86-110
singles/components/SinglesTableSkeleton.tsx:77-101
src/components/table-skeleton.tsx:100-124
25 89
collections/config/validate-config.ts:380-433
field-groups/config/validate-field-group.ts:185-238
singles/config/validate-single.ts:190-243
54 152
dispatcher/handlers/collection-dispatcher.ts:925-967
field-groups/services/field-group-table-provisioning.ts:186-236
singles/services/reconcile-single-companion.ts:110-160
51 149

… and 672 more groups.

Across 425 files.

Complexity (1035 functions above threshold)
File Function Severity Cyclomatic Cognitive CRAP Lines
singles/services/single-mutation-service.ts:966 <arrow> critical 246 ! 308 ! 13317.5 ! 1650
collections/services/collection-mutation-service.ts:6329 <arrow> critical 165 ! 153 ! 6045.6 ! 1261
src/init/reload-config.ts:1417 applyReload critical 143 ! 211 ! 4560 ! 1470
shared/lib/entry-validation.ts:245 validateFieldValue critical 109 ! 157 ! 2675.3 ! 432
dynamic-collections/services/dynamic-collection-schema-service.ts:941 generateAlterTableMigration critical 103 ! 220 ! 2394.5 ! 738

5105 files, 79097 functions analyzed (thresholds: cyclomatic > 20, cognitive > 15, CRAP >= 30)

Codebase health

Metric Value
Maintainability 91.7 / 100
Avg complexity 1.8

Tip

Run fallow fix --dry-run to preview auto-fixes.
Add /** @public */ above exports to preserve them.

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

Superseded by #1844, which merged carrying this same change: every collection field-access call site on main now passes params.authenticatedScope, verified at all seven of them.

Splitting it out was the right call at the time, when #1844 was being reworked and I did not want a clean measured fix held behind a subtle one. #1844 merging made that moot. The tests in this branch are worth keeping, so they come back with the follow-up rather than being lost here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: core nextly type: docs Documentation only

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant