Skip to content

feat(organizations): role-aware actions, visibility, and three wire fixes (#83) - #115

Merged
Adron merged 1 commit into
parity/queuefrom
issue/83-org-roles-visibility
Sep 16, 2026
Merged

Adron merged 1 commit into
parity/queuefrom
issue/83-org-roles-visibility

Conversation

@Adron

@Adron Adron commented Sep 16, 2026

Copy link
Copy Markdown
Member

Closes #83. Part of epic #80.

The role model came from the help centre, verbatim

/help/organizations states it plainly, and the wording is used as-is:

  • Owner — "Full control; can delete the org and manage all members"
  • Admin — "Can add and remove members and change roles (except owner)"
  • Member — "Basic access"
  • "The last remaining owner cannot be demoted or removed."
  • "You cannot leave the system "The Public" organization."

That settles the matrix in OrgPermissions.kt: add member / edit org = owner+admin; delete org =
owner only; change-role and remove = owner on anyone, admin on anyone except an owner and never
granting owner; view members = any member; join = non-member of a public org; leave = any member of
a non-system org.

What changed

  • The non-member Edit/Delete gap Organizations: join and leave #81 flagged is closed. The overflow renders only permitted
    items and disappears entirely when a role has none — a non-member gets no menu at all.
  • Member rows are role-aware. Role chips are restricted to what the viewer may actually assign
    (an admin gets no Owner chip, and only a read-only label on an owner's row); Remove is hidden where
    it would be refused; a plain member sees a read-only roster.
  • Visibility is surfaced and editable — "Public · anyone can see and join" / "Private ·
    invite-only; an owner or admin adds members", plus the viewer's role and a system-org marker.
    PUT /api/organizations/{id}, field isPublic, owner+admin.
  • Last-owner rules honoured both ways, matching Organizations: join and leave #81's approach to leave: the sole owner is
    offered no demotion or removal and gets an inline explanation, and the server's
    Cannot demote the last owner / Cannot remove the last owner 400s map to the same text. Guards
    live in the ViewModel too, so hiding is genuinely UX and not the only barrier.
  • isSystem handled: the help centre says you cannot leave "The Public", so system orgs offer
    neither Leave nor Delete.

Three live-verified wire bugs found and fixed

  1. isPublic was serialised as a string on both create and update. The server answers
    500 {"error":"Internal server error"} to "isPublic":"false" and 200 to "isPublic":false.
    Visibility never round-tripped from the app at all. Now a real JSON boolean.
  2. The PUT echo omits userRole and memberCount. Mapping it straight through made the owner
    who had just edited look like a non-member — hiding the very actions they had just used.
    updateOrganization now re-reads.
  3. isSystem was not modelled, which is why Leave was offered on "The Public". Added to the DTO,
    entity (Room v1→v2, destructive migration already configured), mapper and domain.

Verification

./gradlew :app:assembleDebug testDebugUnitTest → BUILD SUCCESSFUL.
:feature:organizations:testDebugUnitTest --rerun-tasks93 tests, 0 failures. New
OrgPermissionsTest has one case per role per action (15), plus 13 ViewModel cases, boolean
wire-body assertions, and the Compose test rewritten for the rendered role rules (compiles, not
executed — no emulator).

No :app change was needed.

Live-API cleanup

One throwaway organization zz-parity-probe-83 was created to probe PUT/role behaviour and was
deleted
; a second create attempt returned 500 and created nothing. Verified afterwards that
GET /api/organizations lists only the four real orgs and GET /api/user/organizations only the
account's original three memberships. Probes against other people's orgs used a non-existent user id
so nothing could be mutated; nothing was changed on "The Public" or any real organization.
(Independently re-verified after the fact.)

Notes and risks

  • The server does not appear to enforce these role rules on the member endpoints. A plain member
    POSTing to /members got a 500 (FK failure), not a 403, and PUT/DELETE on a member returned
    404 "User is not a member of this organization" rather than a permission error. So the UI
    gating is the practical protection here
    — worth knowing, and arguably worth a server-side fix.
  • The admin-vs-owner split for edit could not be confirmed live (one account, member everywhere).
    It follows the help centre: edit = owner+admin, delete = owner only. If that is wrong,
    OrgPermissions is the single place to change it.
  • active / suspend is not implemented. The docs mention suspending a member, and the server
    requires role alongside active ({"active":false} alone → 400 "Valid role (owner, admin, or member) is required"). Out of scope here, captured for later.

The detail screen offered Edit and Delete to everyone, including
non-members (#81 flagged this), and offered every role chip and a Remove
button on every member row regardless of what the signed-in user could
actually do. Calls that the server would refuse were simply sent.

Model the documented role rules once, in OrgPermissions, quoting
/help/organizations rather than inventing a hierarchy:

- owner: full control; the only role that may delete the organization
- admin: add/remove members and change roles, except an owner's, and may
  not hand out ownership
- member: basic access — the roster is visible but read-only
- non-member: nothing; the members endpoint is members-only (403)
- a system organization ("The Public") can be neither left nor deleted

The detail screen now renders only the permitted affordances: the
overflow disappears entirely when nothing is available, the member
picker is owners/admins only, role chips are limited to the roles the
viewer may assign, and Remove is hidden where it would be refused.

The server stays authoritative — hiding is UX, not enforcement — so
every rejection still surfaces, and the last-owner rules are explained
before and after the request for demote and remove, matching how #81
handled leave.

Visibility is stated on the header (with what public/private mean) and
remains editable by the roles permitted to edit the organization.

Three wire bugs found while probing the live API, all fixed:

- isPublic was serialised as a string on both create and update; the
  server answers 500 to a string, so visibility never round-tripped
- the PUT echo carries no userRole/memberCount, so mapping it straight
  through made the editor look like a non-member and hid the actions
  they had just used; the repository re-reads instead
- isSystem was not modelled at all, so "The Public" offered Leave

Tests: a role x action matrix in OrgPermissionsTest, per-role view model
coverage including a non-member seeing no Edit/Delete, last-owner demote
and remove explained both before the call and on the server's rejection,
visibility read and round-tripped, plus Compose coverage of the same
rules on the rendered screen.

Closes #83
@Adron
Adron merged commit 3f76a69 into parity/queue Sep 16, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant