-
Notifications
You must be signed in to change notification settings - Fork 3
security(users): /users/:id status, role and delete act on the global account, not on the caller's membership #807
Copy link
Copy link
Open
Labels
area:backendGo, /internal, /pkgGo, /internal, /pkgarea:securityCybersecurity and threat intelligenceCybersecurity and threat intelligencepriority:P0Blocking: nothing else ships until this closesBlocking: nothing else ships until this closespriority:P0-criticalProduction broken or exposed — work nowProduction broken or exposed — work nowstatus:readyMeets the ready definitionMeets the ready definitiontier:0-trustTrust: security, isolation, evidence integrityTrust: security, isolation, evidence integritytrustEvidence a buyer's CISO tests before features matterEvidence a buyer's CISO tests before features mattertype:securitySecurity defect or hardeningSecurity defect or hardening
Milestone
Description
Activity
Metadata
Metadata
Assignees
Labels
area:backendGo, /internal, /pkgGo, /internal, /pkgarea:securityCybersecurity and threat intelligenceCybersecurity and threat intelligencepriority:P0Blocking: nothing else ships until this closesBlocking: nothing else ships until this closespriority:P0-criticalProduction broken or exposed — work nowProduction broken or exposed — work nowstatus:readyMeets the ready definitionMeets the ready definitiontier:0-trustTrust: security, isolation, evidence integrityTrust: security, isolation, evidence integritytrustEvidence a buyer's CISO tests before features matterEvidence a buyer's CISO tests before features mattertype:securitySecurity defect or hardeningSecurity defect or hardening
Problem
An administrator of organization A can lock a person out of every organization they belong to, or delete their account everywhere. They can also "change their role" and get a success message for a change that has no effect. All three go through routes that write the global
usersrow instead of the caller's membership.Routes:
backend/cmd/server/main.go:2052-2054. Handlers:backend/internal/handler/user_handler.go.PATCH /users/:id/statususers.is_active(l.157-168)internal/application/auth/login.go:182). Deactivating someone in A locks them out of B.DELETE /users/:idusersrow (l.289)PATCH /users/:id/roleusers.role_id(l.227-241)organization_members.role(theGetOrganizationMemberblock inlogin.go). The write is ignored, but the response says"User role updated".The admin check inside these handlers (
currentUser.Role.Name != "admin", l.146, 212, 269) reads the global role. #702 has the same root cause.userInTenant()(l.70) only checks that the target is a member of the caller's tenant. It does not limit the effect to that tenant.The UI no longer calls these routes. Settings uses
/organization/members/:memberId/{role,status}(membership service,main.goaround l.2545).useUsers()infrontend/src/features/settings/adminData.ts:24is imported nowhere. The routes can still be reached by any admin API caller.Per CLAUDE.md rule 2, a write whose effect crosses the tenant boundary is a P0 security defect.
Found during the direction audit of 2026-09-25 (code read, not yet reproduced live; criterion 2 is the reproduction).
Acceptance criteria
PATCH /users/:id/status,PATCH /users/:id/roleandDELETE /users/:idare removed from the router. If a live consumer turns up, they are reimplemented as membership operations on the caller's tenant only. The PR records which of the two was done.users.role_id. Admin checks go throughRequirePermissionor the membership role. A test proves that settingusers.role_id = admingrants nothing.useUsers()hook and theAdminUsertype are removed fromfrontend/src/features/settings/adminData.ts, andnpm run type-checkis green.Success,NotFound(a foreign or unknown member gets a 404 with the same body either way) andUnauthorized(a non-admin gets a 403).docs/openapi.yamlno longer lists the removed routes, andinternal/handler/authz_route_coverage_test.gois updated to match.Definition of Done
No request made in the context of tenant A can change whether a person can sign in to tenant B, or what they can do there. The cross-tenant test output is pasted on this issue.
Out of scope
POST /usersreturning 500 is tracked in fix(users): POST /users answers 500, so the E2E seed cannot provision analyst or auditor #590.