Skip to content

Server edition: agent-token names are a global namespace and AuthContext() drops UserID, so a tenant's token carries no user identity #1168

Closed

Description

@Dumbris

Summary

Two related gaps in how the server edition's per-user agent tokens map onto the core token store. Neither leaks a credential on its own, but together they mean a tenant's token has no user identity downstream and the token name space is shared across all users.

1. Global name namespace

internal/storage/agent_tokens.go keys the name→hash mapping in a single AgentTokenNamesBucket by the bare name, with no user scoping. GetAgentTokenByName(name) reads that global bucket ignoring UserID.

Consequences in the server edition, where every user creates tokens at POST /api/v1/user/tokens (internal/serveredition/api/user_handlers.go:707):

  • Two users cannot both have a token called ci. The second gets a conflict.
  • The 403-vs-409 split is an oracle: revokeUserToken (user_handlers.go:~752) returns 403 "Cannot revoke another user's token" when the name exists but belongs to someone else, and 404 when it does not exist — so any user can probe other users' token names.

2. AuthContext() drops UserID

internal/auth/agent_token.go:54-66 copies Type, AgentName, TokenPrefix, AllowedServers, Permissions and ProfilePin — but not UserID, which the server edition stores on the token (user_handlers.go:705-712). Both middlewares build the context through this single constructor, so once a request is authenticated by an agent token there is no way to tell which tenant it belongs to.

That matters wherever a downstream surface wants a per-user answer rather than a per-token one: per-user credential resolution, per-user activity attribution, and any future per-tenant filtering. It is also why the enumeration gap in #1166 cannot currently be fixed by user rather than by token scope.

Note the constructor's own doc comment says it exists "so no auth path can silently drop a field" — UserID is exactly that failure, one field later.

Suggested direction

  1. Scope the name bucket by user (or store a composite key) and make GetAgentTokenByName take the owner, so names are per-user and the revoke path stops answering differently for another user's token.
  2. Carry UserID through AgentToken.AuthContext(), and add it to the constructor's field-parity test if one exists.

Both are storage/format changes, so they want a migration plan for existing tokens.

Found during the #1161 residual sweep (#1165); out of scope there.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/teamsServer edition multi-user / teams featureskind/bugSomething isn't workingkind/securitySecurity fix or hardening

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions