Skip to content

feat(members): promote/demote org admins + fix invite-as-admin grant#433

Merged
angel-manuel merged 2 commits into
devfrom
feat/org-admin-promote-demote
Jul 6, 2026
Merged

feat(members): promote/demote org admins + fix invite-as-admin grant#433
angel-manuel merged 2 commits into
devfrom
feat/org-admin-promote-demote

Conversation

@angel-manuel

Copy link
Copy Markdown
Contributor

Summary

Two related fixes to org admin management. Today, promoting/demoting an admin is only possible via direct psql, and inviting someone "as admin" doesn't actually grant admin.

1. NEW — promote/demote existing members

Adds an AdminAcl-gated PATCH /v1/org-members/{identity_id} taking { "role": "admin" | "member" }.

Admin authorization depends on three signals that had to move together — the change flips all of them atomically:

  • user_org_memberships.role (last-admin guard + display)
  • identities.is_org_admin flag (fast-path in OrgAcl)
  • membership in the bootstrap Admins group (the group-grant path AdminAcl reads)

Guards the last-admin invariant: refuses to demote the final admin, including self-demotion (mirrors remove_user_from_org). Locks admin membership rows in user_id order so concurrent role changes serialise instead of deadlocking.

2. BUG — invite-as-admin conferred no real admin

POST /v1/org-invites accepts role: "admin", but the acceptance/JIT path only wrote membership::create(.., role). An invited "admin" got memberships.role='admin' yet failed AdminAcl (no is_org_admin flag, not in the Admins group).

Now the acceptance path confers real admin (flag + group) when the invite role is admin, unified with the existing second-IdP admin-mirror. Both go through the shared set_is_org_admin primitive so the membership role, flag, and group can never diverge.

Shared primitive

Extracted sync_admins_group_tx; both set_is_org_admin (one identity — used by invite-accept) and the new atomic set_org_member_admin (a whole membership — used by the endpoint) route through it. No divergent admin-granting logic.

Dashboard (vertical integration)

members/+page.svelte gains a Role column/badge and an admin-only Make admin / Remove admin control in the member drawer. is_org_admin is now exposed on IdentityResponse; the page reads viewer admin state from the layout to gate the control.

Testing

  • cargo test -p overslash-api — new org_member_role.rs (promote → passes AdminAcl; demote → loses it; last-admin refusal incl. self; non-admin 403; invalid role) all green; extended overslash_managed_signin.rs to assert an invited admin passes an AdminAcl-gated request. Adjacent identity_remove_user.rs still green.
  • cargo clippy clean, cargo fmt clean, svelte-check 0 errors, strict dashboard build passes.
  • /vet — no issues.

Screenshots

Members page admin control — to be attached via the release-asset convention.

Notes

  • No schema migration (all columns/tables already exist).
  • Overlaps task 8ef58 (domain-JIT): the shared set_is_org_admin call is the single place the invite/JIT path grants admin.

🤖 Generated with Claude Code

Two related fixes to org admin management.

1. NEW — promote/demote existing members. Adds an AdminAcl-gated
   `PATCH /v1/org-members/{identity_id}` with `{ role: admin|member }`.
   Promotion/demotion is atomic across all three admin signals that
   authorization depends on: `user_org_memberships.role`, the per-identity
   `is_org_admin` flag, and `Admins`-group membership (the surface
   `AdminAcl`/`OrgAcl` actually read). Enforces the last-admin invariant
   (refuses to demote the final admin, including self-demotion). Previously
   only possible via direct psql.

2. BUG — invite-as-admin conferred no real admin. The invite-accept/JIT path
   wrote only `membership::create(.., role)`; an invited "admin" got
   `role='admin'` but failed `AdminAcl` (no flag, not in Admins group). Now
   the acceptance path grants real admin (flag + group) when the invite role
   is admin, unified with the existing second-IdP admin mirror through the
   shared `set_is_org_admin` primitive.

Shared primitive: extracted `sync_admins_group_tx`; both `set_is_org_admin`
(single identity, used by invite-accept) and the new atomic
`set_org_member_admin` (whole membership, used by the endpoint) route through
it so the flag and group can never drift.

Dashboard: Members page gains a Role column/badge and an admin-only
promote/demote control in the member drawer (`is_org_admin` now exposed on
IdentityResponse).

Tests: promote→real admin (passes AdminAcl), demote→loses AdminAcl,
last-admin refusal (incl. self), non-admin 403, invalid role; extended the
managed-signin suite to assert an invited admin passes an AdminAcl request.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
overslash Ready Ready Preview, Comment Jul 6, 2026 5:23pm

Request Review

Comment thread dashboard/src/routes/members/+page.svelte
Seer review: the +page.ts comment claimed we hide self-targeting
affordances, but the drawer intentionally allows an admin to demote
themselves when other admins remain (surfacing a warning); the backend
still refuses demoting the last admin. Reworded the comment to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.85294% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/overslash-api/src/routes/org_members.rs 91.80% 5 Missing ⚠️
crates/overslash-api/src/lib.rs 0.00% 1 Missing ⚠️
crates/overslash-db/src/repos/identity.rs 98.21% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@angel-manuel angel-manuel merged commit fcf8d24 into dev Jul 6, 2026
15 checks passed
@angel-manuel angel-manuel deleted the feat/org-admin-promote-demote branch July 6, 2026 17:46
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