Skip to content

chore(web): add ESLint rule require-auth-wrapper#1199

Merged
brendan-kellam merged 3 commits into
mainfrom
brendan/eslint-require-auth-wrapper
May 13, 2026
Merged

chore(web): add ESLint rule require-auth-wrapper#1199
brendan-kellam merged 3 commits into
mainfrom
brendan/eslint-require-auth-wrapper

Conversation

@brendan-kellam
Copy link
Copy Markdown
Contributor

@brendan-kellam brendan-kellam commented May 13, 2026

Summary

Adds an authz/require-auth-wrapper ESLint rule that fires on exported API route handlers (under packages/web/src/app/api/**/route.ts) and server-action exports (files with a 'use server' directive) whose body doesn't textually reference withAuth( or withOptionalAuth(. Goal: catch endpoints that ship without an auth wrapper before they merge.

  • Boundary-only by design — the rule does not trace through helper functions. A route that delegates auth into a helper must either inline the wrapper at the boundary or carry an allowlist comment with a reason.
  • withMinimumOrgRole alone does not satisfy — it always nests inside withAuth, so missing withAuth makes the role gate meaningless.
  • Allowlist via standard // eslint-disable-next-line authz/require-auth-wrapper -- <reason>.
  • 47 existing violations resolved by allowlisting (none required a code fix — auth was correct, just below the boundary the rule inspects). Three groups: delegate-to-helper routes (19), genuinely public endpoints (10), and pre-membership or UI-only server actions (9).

Test plan

  • yarn workspace @sourcebot/web lint runs cleanly against the rule (no new errors beyond pre-existing unrelated ones)
  • yarn workspace @sourcebot/web vitest run tools/eslint-plugin-local — rule unit tests pass; covers withAuth, withOptionalAuth, neither, withMinimumOrgRole-only, allowlist comment, action with/without wrapper, non-route/non-action file, destructured export const { GET, POST } = ..., and re-export specifiers (export { handler as GET })
  • Spot-check: temporarily add export const GET = async () => Response.json({ leak: true }); to a route file and confirm lint fails

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a new ESLint rule to enforce authorization wrapper usage on API route handlers and server actions, improving security consistency across the codebase.
  • Chores

    • Updated ESLint configuration to enable the new authorization rule.
    • Documented authorization handling patterns for existing endpoints and server actions where authorization is delegated or intentionally bypassed.

Review Change Stack

Adds an authz/require-auth-wrapper rule that flags exported route handlers
and server actions whose body doesn't textually reference withAuth() or
withOptionalAuth(). The check is boundary-only by design — false positives
are allowlisted with a // eslint-disable-next-line comment plus a reason.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

Walkthrough

This PR introduces a new ESLint rule, authz/require-auth-wrapper, that enforces API route handlers and server actions to explicitly wrap authentication with withAuth() or withOptionalAuth(). The implementation includes the rule logic, plugin registration, comprehensive tests, and exemption annotations throughout the codebase documenting delegated auth patterns.

Changes

Authorization Wrapper ESLint Rule & Exemptions

Layer / File(s) Summary
Auth wrapper enforcement rule implementation
packages/web/tools/eslint-plugin-local/rules/requireAuthWrapper.mjs
The require-auth-wrapper ESLint rule detects missing auth wrappers by parsing API route filenames, top-level "use server" directives, and exported function bodies; classifies exports as routes or actions; and reports violations when withAuth() / withOptionalAuth() patterns are not found, supporting multiple export syntaxes including destructured and re-export forms.
ESLint plugin registration and configuration
packages/web/tools/eslint-plugin-local/index.mjs, packages/web/eslint.config.mjs
The plugin object is created with metadata and rule registration, then integrated into the flat ESLint config to enable authz/require-auth-wrapper with error severity.
Rule test suite
packages/web/tools/eslint-plugin-local/rules/requireAuthWrapper.test.mjs
Vitest test suite with comprehensive fixtures covering valid route handlers and server actions with proper wrappers, allowlist comment bypasses, and invalid cases asserting expected diagnostic messages for multiple export syntaxes.
Changelog entry
CHANGELOG.md
Documents the addition of the new authz/require-auth-wrapper ESLint rule under the unreleased section.
Auth exemptions across API routes and server actions
packages/web/src/app/api/(server)/*, packages/web/src/actions.ts, packages/web/src/app/invite/actions.ts, packages/web/src/ee/features/sso/actions.ts, packages/web/src/features/chat/actions.ts
Exemption comments are added throughout the codebase documenting where authentication is delegated to downstream functions (getFileBlame(), listCommits(), getTree(), etc.), handled by NextAuth flow, or bypassed for pre-membership flows and UI-only operations.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • sourcebot-dev/sourcebot#1073: Refactors withAuthV2 / withOptionalAuthV2 to withAuth / withOptionalAuth across the codebase, directly impacting the wrapper patterns this new ESLint rule enforces.
  • sourcebot-dev/sourcebot#1064: Modifies the GET handler in packages/web/src/app/api/(server)/mcp/route.ts while this PR adds an authz/require-auth-wrapper exemption for the same route.
  • sourcebot-dev/sourcebot#976: Originally implements packages/web/src/app/api/(server)/mcp/route.ts, which this PR now exempts from the new auth wrapper rule.

Suggested labels

sourcebot-team

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'chore(web): add ESLint rule require-auth-wrapper' directly and clearly summarizes the main change: adding a new ESLint rule to enforce authentication wrapper usage.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch brendan/eslint-require-auth-wrapper

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 and usage tips.

@github-actions

This comment has been minimized.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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/web/src/app/api/(server)/ee/oauth/register/route.ts (1)

17-68: ⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoff

Add rate limiting to the unauthenticated OAuth client registration endpoint.

The /api/ee/oauth/register endpoint allows unauthenticated client creation per RFC 7591, but lacks rate limiting or abuse protections. Without throttling, the endpoint is vulnerable to DoS attacks (excessive requests) and database bloat (rapid client creation). Implement rate limiting using IP address, trusted client origin, or both.

🤖 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/web/src/app/api/`(server)/ee/oauth/register/route.ts around lines 17
- 68, Add request throttling to the unauthenticated POST handler exported as
POST (wrapped by oauthApiHandler) to prevent mass client creation; before
parsing the body and creating a record via __unsafePrisma.oAuthClient.create,
check a rate-limit store keyed by requester IP (and optionally by Origin header
for trusted origins) and reject when limits are exceeded with a 429 JSON
response. Implement or call a shared rateLimit helper (token-bucket or
fixed-window) that exposes e.g. allowRequest(ipOrKey) and remaining/reset
metadata, enforce a low quota for anonymous registration attempts, and
short-circuit the handler to return Response.json({ error:
'rate_limit_exceeded', error_description: 'Too many registration attempts' }, {
status: 429 }) when allowRequest returns false; ensure the check runs before DB
access and that legitimate trusted origins can be whitelisted by consulting
Origin header or a config list.
🧹 Nitpick comments (1)
packages/web/src/actions.ts (1)

856-862: ⚡ Quick win

Redundant condition can be removed.

The if (!existingRequest) check on line 856 is always true at this point because if existingRequest were truthy, the function would have returned early on lines 848–853. You can safely remove this conditional wrapper and unindent the block.

♻️ Simplify by removing redundant check
-    if (!existingRequest) {
-        await __unsafePrisma.accountRequest.create({
-            data: {
-                requestedById: user.id,
-                orgId: org.id,
-            },
-        });
+    await __unsafePrisma.accountRequest.create({
+        data: {
+            requestedById: user.id,
+            orgId: org.id,
+        },
+    });

Then unindent lines 863–915 by one level.

🤖 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/web/src/actions.ts` around lines 856 - 862, The check "if
(!existingRequest)" is redundant because the function already returns when
existingRequest is truthy (lines with that early return), so remove the
conditional wrapper and unindent the block that calls
__unsafePrisma.accountRequest.create({ data: { requestedById: user.id, orgId:
org.id } }); (and the subsequent lines that were nested under that if) so the
create call runs directly at the same scope as the earlier return; update code
around the existingRequest reference accordingly to keep logic intact.
🤖 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/web/src/app/api/`(server)/ee/oauth/register/route.ts:
- Around line 17-68: Add request throttling to the unauthenticated POST handler
exported as POST (wrapped by oauthApiHandler) to prevent mass client creation;
before parsing the body and creating a record via
__unsafePrisma.oAuthClient.create, check a rate-limit store keyed by requester
IP (and optionally by Origin header for trusted origins) and reject when limits
are exceeded with a 429 JSON response. Implement or call a shared rateLimit
helper (token-bucket or fixed-window) that exposes e.g. allowRequest(ipOrKey)
and remaining/reset metadata, enforce a low quota for anonymous registration
attempts, and short-circuit the handler to return Response.json({ error:
'rate_limit_exceeded', error_description: 'Too many registration attempts' }, {
status: 429 }) when allowRequest returns false; ensure the check runs before DB
access and that legitimate trusted origins can be whitelisted by consulting
Origin header or a config list.

---

Nitpick comments:
In `@packages/web/src/actions.ts`:
- Around line 856-862: The check "if (!existingRequest)" is redundant because
the function already returns when existingRequest is truthy (lines with that
early return), so remove the conditional wrapper and unindent the block that
calls __unsafePrisma.accountRequest.create({ data: { requestedById: user.id,
orgId: org.id } }); (and the subsequent lines that were nested under that if) so
the create call runs directly at the same scope as the earlier return; update
code around the existingRequest reference accordingly to keep logic intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f7a255d7-b94e-4879-a748-d173804d5c71

📥 Commits

Reviewing files that changed from the base of the PR and between f1510c8 and 07b47d4.

📒 Files selected for processing (41)
  • CHANGELOG.md
  • packages/web/eslint.config.mjs
  • packages/web/src/actions.ts
  • packages/web/src/app/api/(server)/[...slug]/route.ts
  • packages/web/src/app/api/(server)/auth/[...nextauth]/route.ts
  • packages/web/src/app/api/(server)/blame/route.ts
  • packages/web/src/app/api/(server)/chat/blocking/route.ts
  • packages/web/src/app/api/(server)/commit/route.ts
  • packages/web/src/app/api/(server)/commits/authors/route.ts
  • packages/web/src/app/api/(server)/commits/route.ts
  • packages/web/src/app/api/(server)/diff/route.ts
  • packages/web/src/app/api/(server)/ee/.well-known/oauth-authorization-server/route.ts
  • packages/web/src/app/api/(server)/ee/.well-known/oauth-protected-resource/[...path]/route.ts
  • packages/web/src/app/api/(server)/ee/accountPermissionSyncJobStatus/route.ts
  • packages/web/src/app/api/(server)/ee/audit/route.ts
  • packages/web/src/app/api/(server)/ee/oauth/register/route.ts
  • packages/web/src/app/api/(server)/ee/oauth/revoke/route.ts
  • packages/web/src/app/api/(server)/ee/oauth/token/route.ts
  • packages/web/src/app/api/(server)/ee/permissionSyncStatus/route.ts
  • packages/web/src/app/api/(server)/files/route.ts
  • packages/web/src/app/api/(server)/find_definitions/route.ts
  • packages/web/src/app/api/(server)/find_references/route.ts
  • packages/web/src/app/api/(server)/health/route.ts
  • packages/web/src/app/api/(server)/mcp/route.ts
  • packages/web/src/app/api/(server)/openapi.json/route.ts
  • packages/web/src/app/api/(server)/repo-status/[repoId]/route.ts
  • packages/web/src/app/api/(server)/repos/route.ts
  • packages/web/src/app/api/(server)/search/route.ts
  • packages/web/src/app/api/(server)/source/route.ts
  • packages/web/src/app/api/(server)/stream_search/route.ts
  • packages/web/src/app/api/(server)/tree/route.ts
  • packages/web/src/app/api/(server)/version/route.ts
  • packages/web/src/app/api/(server)/webhook/route.ts
  • packages/web/src/app/api/minidenticon/route.ts
  • packages/web/src/app/api/repos/[repoId]/image/route.ts
  • packages/web/src/app/invite/actions.ts
  • packages/web/src/ee/features/sso/actions.ts
  • packages/web/src/features/chat/actions.ts
  • packages/web/tools/eslint-plugin-local/index.mjs
  • packages/web/tools/eslint-plugin-local/rules/requireAuthWrapper.mjs
  • packages/web/tools/eslint-plugin-local/rules/requireAuthWrapper.test.mjs

Removed mention of the `authz/require-auth-wrapper` ESLint rule from the changelog.
@brendan-kellam brendan-kellam merged commit 70b8c70 into main May 13, 2026
7 checks passed
@brendan-kellam brendan-kellam deleted the brendan/eslint-require-auth-wrapper branch May 13, 2026 03:13
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.

1 participant