Per-caller Gitea PAT pass-through (replaces shared default token for real users) #59

Closed
opened 2026-07-22 06:22:40 +00:00 by mathias · 1 comment
Owner

Problem

Every upstream Gitea call used the single GITEA_MCP_DEFAULT_TOKEN service PAT, regardless of caller. auth.Caller(ctx) (from oauth2-proxy/Authentik forward-auth headers) was only used for cosmetic "on behalf of X" comment/PR footers — never for actual authorization. With more than one real Gitea user now on the horizon (Authentik SSO wired up for git.d-ma.be, see infra commit a32801c), every caller effectively acted with the service account's permissions.

Fix

PassthroughMiddleware (internal/auth/passthrough.go): if the request's bearer token validates directly against Gitea's own /api/v1/user (Client.ValidateToken), it's used for every upstream call this request makes (gitea.WithToken context override, read in Client.doOnce/doRaw), and the caller identity is set from Gitea's own login name — not the shared default token. Any other bearer (static token, JWT, or none) falls through unchanged to the existing chassis static/JWT auth — this only adds a capability, doesn't remove the existing paths.

Each user authenticates the MCP connection with their own Gitea PAT (minted from their own, Authentik-linked, Gitea account). Gitea's own permission model then gates what they can touch.

Deferred

GITEA_MCP_ALLOWED_OWNERS (default mathias) still gates at the MCP layer regardless of which token authenticated. Worth revisiting now that per-user identity is real — either widen it or make it derive from the caller's own Gitea permissions instead of a static allowlist.

Acceptance criteria (met)

  • internal/gitea: WithToken/TokenFromContext context override, preferred over the client's configured default token in both doOnce and doRaw.
  • internal/gitea: ValidateToken(ctx, token) — resolves a token's Gitea login via /api/v1/user.
  • internal/auth: PassthroughMiddleware — valid Gitea PAT → straight to the MCP server with caller+token in context; invalid/absent → falls through to the existing chassis auth chain.
  • go test -race ./... and task check green.
## Problem Every upstream Gitea call used the single `GITEA_MCP_DEFAULT_TOKEN` service PAT, regardless of caller. `auth.Caller(ctx)` (from oauth2-proxy/Authentik forward-auth headers) was only used for cosmetic "on behalf of X" comment/PR footers — never for actual authorization. With more than one real Gitea user now on the horizon (Authentik SSO wired up for git.d-ma.be, see infra commit `a32801c`), every caller effectively acted with the service account's permissions. ## Fix `PassthroughMiddleware` (`internal/auth/passthrough.go`): if the request's bearer token validates directly against Gitea's own `/api/v1/user` (`Client.ValidateToken`), it's used for every upstream call this request makes (`gitea.WithToken` context override, read in `Client.doOnce`/`doRaw`), and the caller identity is set from Gitea's own login name — not the shared default token. Any other bearer (static token, JWT, or none) falls through unchanged to the existing chassis static/JWT auth — this only adds a capability, doesn't remove the existing paths. Each user authenticates the MCP connection with their own Gitea PAT (minted from their own, Authentik-linked, Gitea account). Gitea's own permission model then gates what they can touch. ## Deferred `GITEA_MCP_ALLOWED_OWNERS` (default `mathias`) still gates at the MCP layer regardless of which token authenticated. Worth revisiting now that per-user identity is real — either widen it or make it derive from the caller's own Gitea permissions instead of a static allowlist. ## Acceptance criteria (met) - `internal/gitea`: `WithToken`/`TokenFromContext` context override, preferred over the client's configured default token in both `doOnce` and `doRaw`. - `internal/gitea`: `ValidateToken(ctx, token)` — resolves a token's Gitea login via `/api/v1/user`. - `internal/auth`: `PassthroughMiddleware` — valid Gitea PAT → straight to the MCP server with caller+token in context; invalid/absent → falls through to the existing chassis auth chain. - `go test -race ./...` and `task check` green.
Author
Owner

Deferred item resolved in 4371404: Allowlist.Check(ctx, owner) now skips the static GITEA_MCP_ALLOWED_OWNERS list entirely when the caller authenticated via their own Gitea PAT (pass-through) — Gitea's own permission model gates that caller precisely, a coarse owner-name list would only ever be wrong in that case (either too strict, blocking a legit second user's own repos, or too permissive if widened blindly). The static list still applies unchanged for the shared static-token/JWT path.

Mechanical follow-through: 41 call sites updated to pass ctx (already in scope everywhere), no other tool behavior changed. task check green.

Deferred item resolved in `4371404`: `Allowlist.Check(ctx, owner)` now skips the static `GITEA_MCP_ALLOWED_OWNERS` list entirely when the caller authenticated via their own Gitea PAT (pass-through) — Gitea's own permission model gates that caller precisely, a coarse owner-name list would only ever be wrong in that case (either too strict, blocking a legit second user's own repos, or too permissive if widened blindly). The static list still applies unchanged for the shared static-token/JWT path. Mechanical follow-through: 41 call sites updated to pass `ctx` (already in scope everywhere), no other tool behavior changed. `task check` green.
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/gitea-mcp#59