Skip to content

feat(broker): enforce X-User-Login and project membership on internal routes - #29

Merged
jpricardo merged 12 commits into
feat/multi-tenancyfrom
feat/internal-routes-user-enforcement
Apr 19, 2026
Merged

feat(broker): enforce X-User-Login and project membership on internal routes#29
jpricardo merged 12 commits into
feat/multi-tenancyfrom
feat/internal-routes-user-enforcement

Conversation

@jpricardo

Copy link
Copy Markdown
Owner

Closes #12

Summary

  • New CheckMembership RPC in logger backed by the existing IsMember query; RPCCheckMembershipArgs added to toolbox
  • New toolbox helpers: GetAPIKeyByID and ListAPIKeysByProject so handlers can scope key operations per-project without cross-project leakage
  • requireUserLogin middleware in broker — rejects requests missing X-User-Login with 401
  • checkProjectMembership helper — dials logger RPC and returns false (→ 403) if the user is not a member of the target project
  • All 6 internal handlers updated: each now requires project_id, validates membership, and scopes its data accordingly
  • Frontend Api class refactored: module-level singleton replaced by createApi(userLogin) factory; every method receives projectId; X-User-Login header sent on all calls
  • Dashboard, Keys, and Settings pages updated to call requireAuth for the user login and read projectId from the ?projectId= URL search param

Acceptance criteria

  • Requests missing X-User-Login are rejected with 401
  • Requests from a user who is not a member of the target project are rejected with 403
  • All six routes scope their data to the given project_id
  • Frontend code updated to send correct headers and params (project context flows from ?projectId= URL param)

Test plan

  • go test ./... passes in broker and toolbox
  • npm run typecheck passes in frontend
  • GET /keys?project_id=<id> without X-User-Login401
  • GET /keys?project_id=<id> with a user not in the project → 403
  • GET /keys?project_id=<id> with a valid member → 200 with scoped keys only
  • Same pattern holds for POST /keys, DELETE /keys/:id, GET /settings/retention, PATCH /settings/retention, GET /metrics

🤖 Generated with Claude Code

… routes (#12)

- Add RPCCheckMembershipArgs to toolbox and CheckMembership RPC to logger,
  backed by the existing IsMember query
- Add GetAPIKeyByID and ListAPIKeysByProject helpers to toolbox so handlers
  can scope key reads/writes to a project without cross-project leakage
- Add requireUserLogin middleware (401 on missing X-User-Login header) and
  checkProjectMembership helper (RPC to logger) to broker
- Wire requireUserLogin into the internal route group alongside
  requireInternalSecret
- Update all 6 internal handlers to require project_id, validate membership
  (403 on non-member), and scope data to the given project
- Update frontend Api class: add X-User-Login header via per-request
  createApi(userLogin) factory; add project_id to all six method signatures
- Update dashboard, keys, and settings loaders/actions to use createApi
  with the authenticated user login and projectId from URL search params

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented Apr 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
logwolf-docs Ready Ready Preview, Comment Apr 19, 2026 10:08pm

jpricardo and others added 9 commits April 19, 2026 18:36
…a call

GetRetention, UpdateRetention, and GetMetrics each opened two sequential
TCP connections to the logger: one inside checkProjectMembership and a
second for the actual data RPC. This doubled latency on those handlers.

Change checkProjectMembership to accept a pre-dialed *rpc.Client instead
of dialing internally. All six internal handlers now dial once, pass the
client to checkProjectMembership, and — for the three handlers that also
need logger RPC for data — reuse that same connection for the data call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GetAPIKeyByID previously propagated mongo.ErrNoDocuments unchanged,
causing the broker to respond with 500. Add a sentinel ErrKeyNotFound to
the toolbox and have GetAPIKeyByID return it when FindOne finds no
document. The RevokeAPIKey handler now checks errors.Is against that
sentinel and replies with 404 Not Found.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
requireUserLogin silently rejected missing X-User-Login headers, making
it impossible to detect misconfigured clients or probing in production
logs. Add a structured JSON log line on deny, matching the format already
used by requireAPIKey.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… absent

When no ?projectId= URL param is present, the loaders previously
forwarded an empty string to the broker, which responded 400 and threw
an unhandled error. Each page now short-circuits in the loader when
projectId is missing and returns noProject:true; the component renders a
brief instructional message instead of hitting an error boundary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ListAPIKeys returned all API keys across all projects and is no longer
called by any handler — the broker now uses ListAPIKeysByProject for
every listing operation. Remove the dead method to avoid confusion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lpers

- broker middleware_test: add three requireUserLogin cases — missing
  header (401), valid header propagated to context, empty header value
  treated as missing (401)
- toolbox apikey_test: verify ErrKeyNotFound sentinel wraps with
  errors.Is, APIKey struct carries ProjectID, RPCCheckMembershipArgs
  fields, and GenerateAPIKey propagates ProjectID to the generated key
- logger rpc_test: verify CheckMembership returns an error (not silent
  false) for both an invalid ObjectID hex and an empty project ID,
  exercising the validation path without requiring a live DB connection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
checkProjectMembership has no dependency on Config state — it only uses
the *rpc.Client passed to it. Remove the unnecessary method receiver so
the function signature clearly communicates that invariant and callers
do not need an app reference to invoke it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All error messages in the auth middleware follow the terse lowercase
pattern established by requireAPIKey ("missing or malformed …").
Change "X-User-Login header is required" to "missing X-User-Login header"
to match that convention.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…aders

Both headers are required by the dashboard's SSR server to reach the
internal broker routes, but neither was listed in the CORS AllowedHeaders
config. Any browser-initiated preflight (e.g. direct JS fetch, integration
test harness) would silently drop them. Add both to keep the CORS
declaration consistent with actual usage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jpricardo and others added 2 commits April 19, 2026 18:59
Document that requireUserLogin must run after requireInternalSecret, as
X-User-Login is caller-supplied and trusted without verification.

Replace noProject discriminant check with a direct null check on metrics
so TypeScript narrows Promise<Metrics>|null correctly — React Router
typegen flattens loader unions, preventing narrowing via the noProject
field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The unscoped ListAPIKeys was removed from toolbox in favour of the
project-scoped variant; update the integration test accordingly and
simplify the assertion to check that no keys remain for the project.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jpricardo
jpricardo merged commit 13ba5da into feat/multi-tenancy Apr 19, 2026
5 checks passed
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