Skip to content

refactor(auth): make AccessControl.boundAuth required, drop dead unauth branch#4181

Open
viktormarinho wants to merge 1 commit into
mainfrom
viktormarinho/access-control-boundauth-required
Open

refactor(auth): make AccessControl.boundAuth required, drop dead unauth branch#4181
viktormarinho wants to merge 1 commit into
mainfrom
viktormarinho/access-control-boundauth-required

Conversation

@viktormarinho

@viktormarinho viktormarinho commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #4180 (the isApiKeyPrincipal flag landed there raised the question: why is boundAuth optional at all?).

Why

boundAuth is built by the context factory for every request — including anonymous ones (no role/perms, then denied by the permission check). So it's never absent at runtime, yet it was typed optional, which left dead "should not happen" guards and a whole unreachable branch.

What

  • boundAuth is now a required constructor param. userId/toolName become required-but-nullable (string | undefined) so the positional signature is legal.
  • Removed the dead !userId && !boundAuth branch in check() — the UnauthorizedError 401 + public-tool fallback. It only ran when boundAuth was missing (impossible). Anonymous callers already get 403 via the permission path, and public tools are gated by the MCP transport's own isPublicTool (auth.ts) and the MESH_PUBLIC_ prefix. With it went the now-unused isToolPublic, the getToolMeta constructor param, the GetToolMetaFn type, and the MCP_MESH_KEY import.
  • Dropped the dead if (!boundAuth) return false guards in checkResource / checkApiKeyAccess / checkMemberAccess.
  • Tests: the bare new AccessControl() constructions now pass a mock boundAuth (still testing grant/granted); the "should deny access when no BoundAuthClient provided" test is deleted (it asserted an impossible state); the "no userId" test now asserts ForbiddenError (the 401 path is gone).

No behavior change — the removed branch never executed in production. Net −50 LOC.

Heads-up / follow-up decision

Removing that branch removed the last throw new UnauthorizedError in the repo, so the type is now never thrown (its instanceof catches in tools-rest.ts/org-fs.ts are dead). Left as-is here. Open question: drop UnauthorizedError + those catches, or restore 401 semantics by making truly-unauthenticated callers throw it (anon → 401 instead of today's 403)? That's a product call, not part of this cleanup.

tsc + oxlint clean; auth+core unit tests green (except the pre-existing DB-gated context-factory.integration.test.ts).

🤖 Generated with Claude Code


Summary by cubic

Make AccessControl.boundAuth a required dependency and remove the unreachable unauthenticated branch to simplify auth checks with no behavior change. Anonymous requests still get 403 via permission checks; public tools continue to be handled upstream in the MCP transport.

  • Refactors
    • Require boundAuth in AccessControl constructor; userId and toolName are now required-but-optional (string | undefined).
    • Remove the dead !userId && !boundAuth path and its public-tool fallback; delete GetToolMetaFn, isToolPublic, MCP_MESH_KEY, and stop passing getToolMeta from the transport and context factory.
    • Drop redundant if (!boundAuth) guards in checkResource, checkApiKeyAccess, and checkMemberAccess.
    • Update tests to pass a mock boundAuth; remove the “no BoundAuthClient” test; unauthenticated case now expects ForbiddenError. UnauthorizedError is no longer thrown, existing catches remain for now.

Written for commit df48818. Summary will update on new commits.

Review in cubic

…th branch

`boundAuth` is built by the context factory for EVERY request (even anonymous),
so it is never absent at runtime. Type it as a required constructor dependency
and delete the code that only ran when it was missing:

- `check()`'s `!userId && !boundAuth` branch (the UnauthorizedError 401 + public-
  tool fallback) was dead in prod — an anonymous caller already gets 403 via the
  permission check, and public tools are gated by the MCP transport's own
  `isPublicTool` and the `MESH_PUBLIC_` prefix. Removed it, along with the
  now-unused `isToolPublic`, the `getToolMeta` constructor param, the
  `GetToolMetaFn` type, and the `MCP_MESH_KEY` import.
- Dropped the dead `if (!boundAuth) return false` guards in `checkResource` /
  `checkApiKeyAccess` / `checkMemberAccess`.
- `userId`/`toolName` become required-but-nullable params (may be undefined, but
  are always passed positionally).

No behavior change: the removed branch never executed in production. Tests that
constructed AccessControl without a boundAuth now pass a mock (grant/granted), or
were deleted (the "no BoundAuthClient" case, which can no longer happen).

Note: AccessControl no longer throws UnauthorizedError (nothing in the repo does
now). The type and its 401 mappings in tools-rest/org-fs are retained pending a
decision on anonymous-caller semantics (401 vs the current 403).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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