Two variants. The first is the ordinary change. The second is the one that matters: the organization's last admin cannot be demoted.
Contract. PUT /v1/security/tenants/{tenantId}/members/{userId}/roles
(RoleAssignment → 200 Member). Replaces the member's assignment.
⚠ The last-admin refusal does not exist yet — this frame proposes it. There is no
last-admin guard in backend/security/src/routes/authz.ts or in the contract today;
the call would succeed and leave the org unadministrable. Approving this frame commissions:
(1) a 409 LAST_ADMIN response on this operation, and (2) a client-side pre-check so
the UI disables the option rather than letting the user discover the refusal by failing.
The disabled control is an affordance, not the enforcement — the server must refuse
independently, because a client-side guard is not a guard.
⚠ Role replacement is destructive today. The provider implements "replace" as
remove-then-re-add, so a partial failure can leave a member with no roles. Until that is
atomic, a failed save must re-fetch and show the member's true current role — never assume
the prior value held.