Skip to content

finding: seven better-auth /admin/ routes refuse platform admins and have no ObjectStack consumer — decide whether they are capability or just vendor surface #9969

Description

@os-warren

Split out of #9652 while implementing its Option-2 fallback. Filed unassigned; observation, not a repro-and-fix.

#9652 measured that better-auth 1.7.1's admin plugin cannot be pointed at ObjectStack's platform-admin predicate, so every route it gates refuses platform admins. Its PR re-mounts the two that a sys_user action calls (ban-user, unban-user); #9968 carries the two that need a decision (impersonate-user, set-role). These seven are the remainder, and what separates them is that a repo-wide grep for their paths finds no ObjectStack consumer at all — no type: 'api' action, no console call, no SDK row that reaches them, no doc that tells a user to call them. They are surface the rawApp.all(${basePath}/*) catch-all publishes because better-auth registers it.

The greppable list, so this does not have to be re-derived:

POST /api/v1/auth/admin/remove-user
POST /api/v1/auth/admin/revoke-user-session
POST /api/v1/auth/admin/revoke-user-sessions
POST /api/v1/auth/admin/list-user-sessions
POST /api/v1/auth/admin/update-user
GET /api/v1/auth/admin/list-users
GET /api/v1/auth/admin/get-user

All seven answer a platform admin 403 YOU_ARE_NOT_ALLOWED_TO_* today, identically to a plain member. They fail closed — this is a broken capability claim, never an escalation.

The decision

Re-implementing all seven as ObjectStack raw mounts is the largest slice of #9652's Option 2 and buys capability nothing in this repo currently pulls. Against that, they ARE published, ledgered (BETTER_AUTH_MOUNTED_SURFACE in auth-route-ledger.ts) and reachable by any client that reads the OpenAPI document, so "advertised but always refuses the only caller entitled to it" is a declared-≠-enforced claim of its own (Prime Directive #10).

Recommendation: B, with A as the standing state — startup scope discipline says do not build seven routes nobody calls, and B makes the trigger explicit so the next author does not have to rediscover why they 403. remove-user is the one worth watching: it is the closest to a real capability (and it carries the break-glass last-local-credential guard in auth-manager.ts's before-hook, which stays intact precisely because that route is not shadowed).

Two mechanics worth recording for whoever picks this up, both measured in #9652:

  1. Shadowing a vendor path with a raw Hono mount silently detaches every better-auth hook keyed on that path. /admin/remove-user carries the break-glass guard. Every better-auth-native /admin/ route refuses ObjectStack platform admins — the vendor gates on the legacy user.role === 'admin' scalar ADR-0068 D2 stopped synthesizing #9652 extracted that guard into last-local-credential.ts so both call sites share one implementation — reuse it rather than reimplementing.
  2. /admin/create-user is the ONLY admin endpoint with the header-less-server-call trust shape (it resolves the session optionally and gates only if (session)). Every other one declares use: [adminMiddleware], which throws UNAUTHORIZED when no session resolves — so the "call it server-side without headers" trick that admin-user-endpoints.ts uses for create-user does not generalise to any of these seven.

Parent: #9652.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions