List users (for assignment pickers and member management)
GET/api/users
List users (for assignment pickers and member management). Pass ?email=<exact-match> to resolve ONE user by email — used by the MCP / pt CLI assignee resolver. Both modes return the SAME envelope: { data: User[] }. The email mode is a one-or-zero-element ARRAY, never a bare object and never null, so this schema describes both modes exactly — a point of repeated confusion in review, since "single user" describes the lookup semantics, not the wire shape. Used by the MCP / pt CLI assignee resolver to translate the INF-125 agent-claim convention email (agent+<class>@constellation.local) into a UUID. The ?email= mode is strictly tenant-scoped to the caller’s active organisation. The listing mode returns the tenant’s own user rows (keyed on identity.users.tenant_id, so synthetic agent users appear whether or not they hold a membership row — customer tenants carry none; the dogfood tenant keeps deliberate org-scoped ones, DIR-69) PLUS members whose ACTIVE user_tenant_memberships row is in this tenant while their user row lives in another — cross-tenant operators (INF-322) — for callers holding FULL directory visibility, which is what makes the listing the assignee-UUID verification set for the MCP / CLI for those callers. Note the listing is not filtered by user status, so soft-deleted and suspended users still appear (the ?email= lookup does filter them; appended cross-tenant rows require the ACTIVE membership itself). FULL directory visibility means an ACTIVE INTERNAL membership plus an ORG-WIDE (GLOBAL-scope) projects.read. Every other caller — a guest, a projects.read.own-only reader, or one whose full read comes only from a project/programme-scoped role assignment — receives the tenant’s own user rows, mirroring their narrower /api/people window; for them find_user can surface a cross-tenant member whose UUID this endpoint does not list (PT-992).
Request
Responses
- 200
- 401
- 403
Successful response
Unauthorized — authentication credentials are missing or invalid
Forbidden, for either of two reasons. (a) The x-act-as-org selector named an organisation the caller is not an ACTIVE member of — getCurrentUser() throws OrgOverrideForbiddenError, which withErrorHandler renders as the canonical envelope. (b) The caller has no active organisation at all (code: "NO_ACTIVE_ORG"), so no tenant scope can be resolved — both modes fail closed rather than fall back to an unscoped listing. Before INF-323 the listing branch queried identity.users with no tenant scope at all, which under the enforced non-bypass RLS role silently returned zero rows and made the MCP reject every valid assignee UUID. This route has NO permission gate, so a 403 here is never a missing-permission denial — those are the only two paths.