Skip to content

refactor(auth): AccessControl — required boundAuth + options-object constructor, drop dead branch#4181

Open
viktormarinho wants to merge 3 commits into
mainfrom
viktormarinho/access-control-boundauth-required
Open

refactor(auth): AccessControl — required boundAuth + options-object constructor, drop dead branch#4181
viktormarinho wants to merge 3 commits 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 there raised: why is boundAuth optional / why are these args positional at all?).

Two related cleanups to AccessControl, both stemming from "boundAuth is always present at runtime":

1. boundAuth is now required + dead branch removed

boundAuth is built by the context factory for every request (even anonymous — no role/perms, then denied by the permission check). So it's never absent, yet it was typed optional, leaving dead guards and an unreachable branch.

  • 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; 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 param, the GetToolMetaFn type, and the MCP_MESH_KEY import.
  • Dropped the dead if (!boundAuth) return false guards in checkResource / checkApiKeyAccess / checkMemberAccess.

2. Single options-object constructor

The 6-positional constructor required three leading optionals and passing undefined to reach boundAuth — a footgun. Replaced with one AccessControlOptions object: boundAuth required, everything else named/optional, connectionId defaulting to "self". TypeScript flagged every call site; all updated (context-factory, MCP transport, automation-context, unit tests).

No behavior change — the removed branch never executed in prod; the rest is a pure call-shape refactor. 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).

Heads-up / follow-up decision

Removing the dead 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. Open question: drop UnauthorizedError + those catches, or restore 401 semantics for unauthenticated callers (anon → 401 vs today's 403)?

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

🤖 Generated with Claude Code

…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>
…tructor

Positional args with three leading optionals (passing `undefined` to reach
`boundAuth`) were a footgun. Replace the 6-positional constructor with one
`AccessControlOptions` object: `boundAuth` required, everything else named and
optional, `connectionId` defaulting to "self". TypeScript flagged every call
site; updated all of them (context-factory, mcp transport, automation-context,
and the unit tests).

No behavior change — pure call-shape refactor on top of the boundAuth-required
cleanup in this PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@viktormarinho viktormarinho changed the title refactor(auth): make AccessControl.boundAuth required, drop dead unauth branch refactor(auth): AccessControl — required boundAuth + options-object constructor, drop dead branch Jun 29, 2026
`createBoundAuthClient` is the sole real producer and always sets it, and
`AccessControl.checkResource` reads it on every call. Typing it optional let an
impossible "bound client without a principal kind" state exist on paper; make it
required so the invariant lives in the type (the flag is read as a real boolean,
no implicit undefined→false coercion). Test mocks cast `as unknown as
BoundAuthClient`, so they're unaffected.

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