You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
organization/remove-member answers a permission denial with 400 "cannot leave as the only owner" — wrong status, wrong reason, false even with two owners #8289
When a caller without permission calls POST /api/v1/auth/organization/remove-member, the request is correctly refused — but the response is:
400 {"message":"You cannot leave the organization as the only owner",
"code":"YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER"}
The caller is not leaving, is not an owner, and — as shown below — the organization does not have only one owner. Every clause of that message is false. The actual reason is "you are a member, you may not remove people".
No security hole: the removal genuinely does not happen (member list unchanged after the attempt). This is about the response being wrong and actively misleading.
Measured
Fixture: workspace with owner lisi, plain member zhangsan (the caller).
2. Add a second owner, so "only owner" is now factually false, and retry
members: [owner:lisi@…, owner:outsider@…, member:zhangsan@…]
actor role: "member"
POST organization/remove-member { memberIdOrEmail: "outsider@…" }
→ 400 YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER
members after: unchanged (all three) ✅ refusal is real
Two owners exist and the target is not the caller, yet the same "only owner" refusal comes back.
3. The sibling endpoint gets it right
POST organization/update-member-role { memberId: <owner>, role: "member" } as the same member
→ 403 YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER ← correct code, correct reason
organization/update, organization/delete and organization/invite-member likewise return proper 403 YOU_ARE_NOT_ALLOWED_TO_* for the same actor. remove-member is the odd one out.
4. The legitimate paths work fine, so this is not a functional defect
as OWNER: remove the other owner → 200, members: [owner:lisi, member:zhangsan]
as OWNER: remove the member → 200, members: [owner:lisi]
Expected
403 with a YOU_ARE_NOT_ALLOWED_TO_REMOVE_THIS_MEMBER-style code, matching the rest of the organization API. The only-owner guard should fire only when it is actually true — i.e. on the self-removal / leave path of a sole owner.
Likely shape of the bug: the permission branch falls through into the leave/last-owner guard instead of returning its own denial, so the last-owner check answers for a question it was never asked.
Why it matters beyond tidiness
A client cannot distinguish "you lack permission" from "business rule blocks this", so it cannot decide whether to hide the action or to surface a retry.
It is a 400 where every sibling denial is a 403, so generic auth-error handling misses it.
objectos-ee-deploy (Caddy → app → postgres:16, NODE_ENV=production, OS_TENANCY_POSTURE=isolated) on http://localhost:8080, observed 2026-08-13. Probed via the REST API with real sessions; a throwaway workspace was used so the owner-removal paths could be exercised safely.
Related: #8092 (the UI affordance that leads a member here).
When a caller without permission calls
POST /api/v1/auth/organization/remove-member, the request is correctly refused — but the response is:The caller is not leaving, is not an owner, and — as shown below — the organization does not have only one owner. Every clause of that message is false. The actual reason is "you are a
member, you may not remove people".No security hole: the removal genuinely does not happen (member list unchanged after the attempt). This is about the response being wrong and actively misleading.
Measured
Fixture: workspace with owner
lisi, plain memberzhangsan(the caller).1. Member tries to remove the owner
2. Add a second owner, so "only owner" is now factually false, and retry
Two owners exist and the target is not the caller, yet the same "only owner" refusal comes back.
3. The sibling endpoint gets it right
organization/update,organization/deleteandorganization/invite-memberlikewise return proper403 YOU_ARE_NOT_ALLOWED_TO_*for the same actor.remove-memberis the odd one out.4. The legitimate paths work fine, so this is not a functional defect
Expected
403with aYOU_ARE_NOT_ALLOWED_TO_REMOVE_THIS_MEMBER-style code, matching the rest of the organization API. The only-owner guard should fire only when it is actually true — i.e. on the self-removal / leave path of a sole owner.Likely shape of the bug: the permission branch falls through into the leave/last-owner guard instead of returning its own denial, so the last-owner check answers for a question it was never asked.
Why it matters beyond tidiness
400where every sibling denial is a403, so generic auth-error handling misses it.memberrole; only the server 403 stops it #8092 (the console shows Remove member to plain members, including on the owner's row), the end-user experience is: click a button you should not have, and be told you cannot leave an organization you were not leaving.Environment
objectos-ee-deploy(Caddy → app → postgres:16,NODE_ENV=production,OS_TENANCY_POSTURE=isolated) onhttp://localhost:8080, observed 2026-08-13. Probed via the REST API with real sessions; a throwaway workspace was used so the owner-removal paths could be exercised safely.Related: #8092 (the UI affordance that leads a member here).