/orgs/{id}/members acts on the organization in the path and judges
permissions from the active one, and the two are routinely different.
Why they diverge
apiClient puts X-Organization-Id: activeOrgId on every request
(frontend/src/lib/api-client.ts:86), so GET /me/permissions answers for the
active organization. The members page reads it through usePermissions() and
decides:
Meanwhile useMembers(id), useInvitations(id) and every write name the org from
the URL.
And nothing keeps them the same: the organizations list opens any org's
members page through an overlay link (orgs/page.tsx:147), while switching the
active org is a separate button that navigates to the dashboard
(orgs/page.tsx:195). So the ordinary way to reach another org's members page is
the way that leaves the active org behind.
What breaks
- Be an Owner of Acme and an Admin of Globex. Have Globex active.
- Organizations → click the Acme card.
- The page lists Acme's members, and every decision about them is Globex's: the
picker offers what an Admin may assign, so Acme's Owner cannot offer Admin on
their own organization.
The inverse is the one that matters more: Admin of Acme, Owner of Globex
active → the picker offers admin, change_role refuses it, and the
offer-then-refuse #1028 removed is back through another door.
Worse than the picker: with members:manage in one organization and not the
other, canManage shows or hides the whole set of controls against the wrong
answer. That half is older than #1028.
Fixed when
The page's decisions and the page's writes name one organization. Two shapes, and
the choice is worth making explicitly:
- Make the URL authoritative. The page (or the
[id] layout) switches the
active organization to the one in the path, so the header and the path agree
for every request the page makes. Cheap, and it makes the org switcher and the
URL one thing rather than two.
- Ask per organization.
/me/permissions takes an org id, and the page asks
for the one it is showing. More surgery, and it leaves two notions of "current"
in the app.
Either way a test opens org B's page with org A active and asserts the controls
describe B.
Related
Found reviewing #1031 (which closes #1028) — the automated review raised the
picker half. Not a duplicate: #1028 was "the picker offers what the service
refuses", and this is "the page asks the wrong organization what the caller may
do". #696 and #700 are the server-side ceiling in the same area.
/orgs/{id}/membersacts on the organization in the path and judgespermissions from the active one, and the two are routinely different.
Why they diverge
apiClientputsX-Organization-Id: activeOrgIdon every request(
frontend/src/lib/api-client.ts:86), soGET /me/permissionsanswers for theactive organization. The members page reads it through
usePermissions()anddecides:
canManage— whether any role control, invite button or spending-limit fieldrenders at all (
page.tsx:91);useAssignableRoles).Meanwhile
useMembers(id),useInvitations(id)and every write name the org fromthe URL.
And nothing keeps them the same: the organizations list opens any org's
members page through an overlay link (
orgs/page.tsx:147), while switching theactive org is a separate button that navigates to the dashboard
(
orgs/page.tsx:195). So the ordinary way to reach another org's members page isthe way that leaves the active org behind.
What breaks
picker offers what an Admin may assign, so Acme's Owner cannot offer Admin on
their own organization.
The inverse is the one that matters more: Admin of Acme, Owner of Globex
active → the picker offers
admin,change_rolerefuses it, and theoffer-then-refuse #1028 removed is back through another door.
Worse than the picker: with
members:managein one organization and not theother,
canManageshows or hides the whole set of controls against the wronganswer. That half is older than #1028.
Fixed when
The page's decisions and the page's writes name one organization. Two shapes, and
the choice is worth making explicitly:
[id]layout) switches theactive organization to the one in the path, so the header and the path agree
for every request the page makes. Cheap, and it makes the org switcher and the
URL one thing rather than two.
/me/permissionstakes an org id, and the page asksfor the one it is showing. More surgery, and it leaves two notions of "current"
in the app.
Either way a test opens org B's page with org A active and asserts the controls
describe B.
Related
Found reviewing #1031 (which closes #1028) — the automated review raised the
picker half. Not a duplicate: #1028 was "the picker offers what the service
refuses", and this is "the page asks the wrong organization what the caller may
do". #696 and #700 are the server-side ceiling in the same area.