Skip to content

feat(structure): Warn when unprotected routes use @requireAuth mutations - #2380

Merged
Tobbe merged 3 commits into
mainfrom
tobbe-check-unprotected-auth-mutations
Aug 12, 2026
Merged

feat(structure): Warn when unprotected routes use @requireAuth mutations#2380
Tobbe merged 3 commits into
mainfrom
tobbe-check-unprotected-auth-mutations

Conversation

@Tobbe

@Tobbe Tobbe commented Aug 11, 2026

Copy link
Copy Markdown
Member

Fixes #2291

Summary

cedar check (via @cedarjs/structure's printDiagnostics) now emits a Warning for routes that are not wrapped in <PrivateSet>/<Private> but whose page — transitively, via its import graph — uses a GraphQL mutation whose root field carries a literal @requireAuth directive in the api-side SDL.

How it works:

  1. SDL directive map (RWProject.requireAuthMutationFields, in packages/structure/src/model/RWProject.ts): a lazy getter that walks RWProject.sdlsRWSDL.implementableFields and builds Map<mutationFieldName, sdlFilePathRelativeToProjectRoot> for every Mutation-type root field carrying a literal @requireAuth directive. Each SDL file is parsed in isolation (try/catch per file) so one malformed SDL doesn't hide valid @requireAuth fields declared elsewhere.

  2. Transitive import walk (packages/structure/src/model/util/pageMutationUsage.ts, new module): given a page's file path, does a BFS over ImportDeclaration module specifiers (resolving relative imports and the src/web/src/ alias, trying .tsx/.ts/.jsx/.js and /index.*), skipping anything that doesn't resolve inside web/src. In each reachable file it looks for gql/graphql tagged template expressions, concatenates the static (quasi) parts of the template, parses it with graphql's parse, and collects the top-level selection field names of any mutation operation definitions. Results are cached per file at the project level (via a WeakMap<RWProject, …>) since many pages share components. A visited set guards against import cycles.

  3. The diagnostic (RWRoute.*diagnostics()): when a route isn't private and its page resolves, intersects the page's transitively-used mutation fields with the project's @requireAuth map and yields a Warning per hit, anchored at the route's JSX node, e.g.:

    Route 'adminPosts' is not wrapped in , but its page uses the mutation 'deletePost', which is marked @requireAuth in api/src/graphql/posts.sdl.ts (found in web/src/components/Post/Post.tsx)

    A new RWError.UNPROTECTED_ROUTE_USES_AUTH_GATED_MUTATION code is attached.

Scoping decisions

  • Mutations only, not queries. Auth-gated queries rendered conditionally on a public page (e.g. showing extra data once logged in) are a common, legitimate pattern; only mutations (which cause side effects) are flagged.
  • Literal @requireAuth only@skipAuth and any custom directives are ignored. Keeps the check simple and avoids false positives from project-specific auth directives with different semantics.
  • DiagnosticSeverity.Warning, never Error — this is a best-effort heuristic (see limitations below), so it must never fail cedar check or block CI; it's a nudge, not a hard rule.

Known limitations

  • Interpolated gql/graphql templates are handled best-effort: the static (quasi) parts of the template are concatenated and the interpolated parts are dropped, which is usually fine for extracting the operation's field shape but can occasionally fail to parse — such documents are silently skipped.
  • Shared components (e.g. a <DeleteButton> used from both a protected and an unprotected route) can cause the warning to fire even when the actual call site guarding is fine at the page level, purely because the mutation is reachable from an unprotected page's import graph. This is a deliberate trade-off — hence Warning, not Error.
  • The import walk does not follow re-exports (export * from './x') or resolve TypeScript path aliases beyond the src/web/src/ convention.

Testing

  • yarn workspace @cedarjs/structure build — pass
  • CI=1 yarn workspace @cedarjs/structure test — 38/38 tests pass (7 test files)
  • New dedicated fixture project at packages/structure/src/model/__tests__/__fixtures__/mutation-auth-check/ (not one of the shared repo-root __fixtures__/ projects, since those are snapshotted by other packages), with new tests in packages/structure/src/model/__tests__/mutationAuthCheck.test.ts covering:
    1. Unprotected route whose page directly uses a @requireAuth mutation → warning, with route/mutation/SDL/component names in the message
    2. Same page, but route wrapped in <PrivateSet><Set>…</Set></PrivateSet> (nested-Set case from fix(structure): Detect Private/PrivateSet ancestors beyond the immediate parent #2379) → no warning
    3. Mutation reached transitively (page → component → component with the gql tag) → warning
    4. Mutation marked @skipAuth → no warning
    5. @requireAuth query (not mutation) on an unprotected page → no warning
    6. Import cycle between two components → terminates without crashing, no warning
  • npx prettier --check / npx eslint on all changed/added files — pass
  • Ran the full structure suite against the existing example-todo-main / example-todo-main-with-errors fixtures used by model.test.ts: no snapshot changes. Verified directly that RWProject.requireAuthMutationFields is empty for both (example-todo-main's mutations are all @skipAuth; example-todo-main-with-errors's only @requireAuth-adjacent field is declared under type Query, not type Mutation, and one of its SDL files has an intentionally-malformed schema string, which is now isolated per-file rather than aborting the whole map).

Stacking

This PR is stacked on #2379 (tobbe-fix-structure-isprivate-nested-sets), which fixed RWRoute.isPrivate to walk nested <Set>/<PrivateSet> JSX ancestors — this PR's nested-Set warning-suppression test (case 2 above) depends on that fix. Base is set to tobbe-fix-structure-isprivate-nested-sets; please retarget to main after #2379 merges.

@github-actions github-actions Bot added this to the next-release milestone Aug 11, 2026
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 798a8afc-4b0a-471c-837a-360467e3125a

📥 Commits

Reviewing files that changed from the base of the PR and between 9c048e2 and 5c0e5f3.

📒 Files selected for processing (1)
  • packages/structure/src/model/__tests__/__fixtures__/mutation-auth-check/cedar.toml

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added diagnostics that warn when unprotected routes use authentication-required GraphQL mutations, including direct and transitive usage.
    • Warnings identify the affected route, mutation, and relevant source locations.
    • Analysis accounts for protected routes, skipped authentication, query-only pages, cyclic imports, and malformed files.
  • Tests

    • Added comprehensive coverage for route and mutation authorization scenarios.

Walkthrough

Changes

Mutation route auth check

Layer / File(s) Summary
Mutation metadata and usage discovery
packages/structure/src/model/RWProject.ts, packages/structure/src/model/util/pageMutationUsage.ts
The project identifies literal @requireAuth mutation fields and traces GraphQL mutations through supported static imports.
Unprotected route diagnostics
packages/structure/src/errors.ts, packages/structure/src/model/RWRoute.ts
Unprotected route pages now emit warnings for transitively used auth-gated mutations, with route, mutation, SDL, and component locations.
Fixture scenarios and end-to-end validation
packages/structure/src/model/__tests__/__fixtures__/mutation-auth-check/*, packages/structure/src/model/__tests__/mutationAuthCheck.test.ts
Fixtures and tests cover direct, protected, transitive, skipped-auth, query-only, and cyclic component cases.

Possibly related issues

Possibly related PRs

  • cedarjs/cedar#2379 — Both PRs extend RWRoute privacy analysis, including nested Private and PrivateSet route handling.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: warning when unprotected routes use @requireAuth mutations.
Description check ✅ Passed The description directly explains the warning behavior, implementation, scope, limitations, tests, and dependency for the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

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

@nx-cloud

nx-cloud Bot commented Aug 11, 2026

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 5c0e5f3

Command Status Duration Result
nx run-many -t build:pack --exclude create-ceda... ✅ Succeeded 5s View ↗
nx run-many -t build ✅ Succeeded <1s View ↗
nx run-many -t build --output-style=stream ✅ Succeeded 1m 20s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-08-12 06:28:29 UTC

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

Actionable comments posted: 7

🤖 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/structure/src/model/__tests__/mutationAuthCheck.test.ts`:
- Around line 115-128: Update the cyclic component fixtures used by
adminPostsCycle to include a `@requireAuth` mutation in one of CycleA or CycleB,
then change the test to assert exactly one
UNPROTECTED_ROUTE_USES_AUTH_GATED_MUTATION warning while retaining the
non-crashing traversal check.
- Around line 26-28: Update the afterEach cleanup for CEDAR_CWD to delete the
process.env property when original_CEDAR_CWD is undefined; otherwise restore the
saved value, preventing a bogus "undefined" environment variable from leaking
between tests.
- Line 17: Update the fixtureDir initialization in mutationAuthCheck.test.ts to
derive the test file directory from import.meta.url using fileURLToPath and
path.dirname, replacing the undefined __dirname reference while preserving the
existing __fixtures__/mutation-auth-check path.

In `@packages/structure/src/model/RWProject.ts`:
- Around line 230-235: Import RWSDLField as a type from ./RWSDLField.js and
annotate the fields variable in the implementableFields handling within
RWProject with RWSDLField[].

In `@packages/structure/src/model/RWRoute.ts`:
- Around line 327-339: In the diagnostic-yielding logic, replace the `as
ExtendedDiagnostic` assertion with a local variable explicitly typed as
`ExtendedDiagnostic`, then yield that variable. Keep the existing object fields
and diagnostic behavior unchanged so TypeScript validates the object structure
directly.

In `@packages/structure/src/model/util/pageMutationUsage.ts`:
- Around line 51-58: Update the queue-processing loop around queue.shift() to
store its result and narrow it with an undefined check before using it as
filePath. Remove the as string assertion, and continue or otherwise safely exit
when shift() returns undefined while preserving the existing visited handling.
- Around line 248-268: Update resolveWithExtensions to strip a trailing .js or
.jsx extension from basePath before testing RESOLVE_EXTENSIONS, so ESM-style
specifiers resolve to corresponding .ts or .tsx sources without producing
candidates such as .js.tsx. Preserve existing direct-file and index resolution
behavior for paths without those extensions.
🪄 Autofix

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

Plan: Pro Plus

Run ID: c2fad306-c060-4712-93ec-bd8d773cb75b

📥 Commits

Reviewing files that changed from the base of the PR and between aba84b1 and eaf4d89.

📒 Files selected for processing (19)
  • packages/structure/src/errors.ts
  • packages/structure/src/model/RWProject.ts
  • packages/structure/src/model/RWRoute.ts
  • packages/structure/src/model/__tests__/__fixtures__/mutation-auth-check/api/src/graphql/posts.sdl.ts
  • packages/structure/src/model/__tests__/__fixtures__/mutation-auth-check/redwood.toml
  • packages/structure/src/model/__tests__/__fixtures__/mutation-auth-check/web/src/Routes.tsx
  • packages/structure/src/model/__tests__/__fixtures__/mutation-auth-check/web/src/components/CycleA/CycleA.tsx
  • packages/structure/src/model/__tests__/__fixtures__/mutation-auth-check/web/src/components/CycleB/CycleB.tsx
  • packages/structure/src/model/__tests__/__fixtures__/mutation-auth-check/web/src/components/DeletePostButton/DeletePostButton.tsx
  • packages/structure/src/model/__tests__/__fixtures__/mutation-auth-check/web/src/components/PostActions/PostActions.tsx
  • packages/structure/src/model/__tests__/__fixtures__/mutation-auth-check/web/src/pages/AdminPostsCyclePage/AdminPostsCyclePage.tsx
  • packages/structure/src/model/__tests__/__fixtures__/mutation-auth-check/web/src/pages/AdminPostsPage/AdminPostsPage.tsx
  • packages/structure/src/model/__tests__/__fixtures__/mutation-auth-check/web/src/pages/AdminPostsProtectedPage/AdminPostsProtectedPage.tsx
  • packages/structure/src/model/__tests__/__fixtures__/mutation-auth-check/web/src/pages/AdminPostsQueryPage/AdminPostsQueryPage.tsx
  • packages/structure/src/model/__tests__/__fixtures__/mutation-auth-check/web/src/pages/AdminPostsSkipAuthPage/AdminPostsSkipAuthPage.tsx
  • packages/structure/src/model/__tests__/__fixtures__/mutation-auth-check/web/src/pages/AdminPostsTransitivePage/AdminPostsTransitivePage.tsx
  • packages/structure/src/model/__tests__/__fixtures__/mutation-auth-check/web/src/pages/HomePage/HomePage.tsx
  • packages/structure/src/model/__tests__/mutationAuthCheck.test.ts
  • packages/structure/src/model/util/pageMutationUsage.ts

Comment thread packages/structure/src/model/__tests__/mutationAuthCheck.test.ts
Comment thread packages/structure/src/model/__tests__/mutationAuthCheck.test.ts
Comment thread packages/structure/src/model/__tests__/mutationAuthCheck.test.ts Outdated
Comment thread packages/structure/src/model/RWProject.ts Outdated
Comment thread packages/structure/src/model/RWRoute.ts Outdated
Comment thread packages/structure/src/model/util/pageMutationUsage.ts
Comment thread packages/structure/src/model/util/pageMutationUsage.ts
Base automatically changed from tobbe-fix-structure-isprivate-nested-sets to main August 11, 2026 22:29
`cedar check` now emits a Warning when a route not wrapped in
<PrivateSet>/<Private> has a page that (transitively, via imports) uses a
GraphQL mutation whose root field carries a literal @requireAuth directive
in the api-side SDL. Mutations only (auth-gated queries on public pages are
a common legitimate pattern), and only Warning severity since shared
components can produce deliberate false positives.

Fixes #2291
@Tobbe
Tobbe force-pushed the tobbe-check-unprotected-auth-mutations branch from eaf4d89 to 2e4f16f Compare August 11, 2026 22:29
@netlify

netlify Bot commented Aug 11, 2026

Copy link
Copy Markdown

Deploy Preview for cedarjs canceled.

Name Link
🔨 Latest commit 5c0e5f3
🔍 Latest deploy log https://app.netlify.com/projects/cedarjs/deploys/6a7c11c069f3310008b409af

Tobbe added 2 commits August 12, 2026 00:35
- Resolve .js/.jsx import specifiers back to their .tsx/.ts source
  in pageMutationUsage.ts, so pages that use extensioned relative
  imports aren't silently skipped by the transitive mutation walk
- Replace `as string`/`as ExtendedDiagnostic` casts with narrowing
  and an explicit local type, matching the repo's casting guidelines
- Annotate the SDL implementable-fields loop variable instead of
  relying on inference
- Fix afterEach leaking the literal string "undefined" into
  process.env.CEDAR_CWD when it wasn't set before the test
- Strengthen the component-cycle test to assert the mutation is
  reported exactly once (dedup), not just that nothing crashes
@Tobbe
Tobbe merged commit 69ae329 into main Aug 12, 2026
46 checks passed
@Tobbe
Tobbe deleted the tobbe-check-unprotected-auth-mutations branch August 12, 2026 06:37
@github-actions

Copy link
Copy Markdown

The changes in this PR are now available on npm.

Try them out by running yarn cedar upgrade -t 6.0.0-canary.2881

Or try it in a new app with yarn dlx create-cedar-app@6.0.0-canary.2881

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.

cedar check: flag routes that call auth-gated mutations but aren't wrapped in PrivateSet

1 participant