feat(members): promote/demote org admins + fix invite-as-admin grant#433
Merged
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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 Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-gatedPATCH /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_adminflag (fast-path inOrgAcl)AdminAclreads)Guards the last-admin invariant: refuses to demote the final admin, including self-demotion (mirrors
remove_user_from_org). Locks admin membership rows inuser_idorder so concurrent role changes serialise instead of deadlocking.2. BUG — invite-as-admin conferred no real admin
POST /v1/org-invitesacceptsrole: "admin", but the acceptance/JIT path only wrotemembership::create(.., role). An invited "admin" gotmemberships.role='admin'yet failedAdminAcl(nois_org_adminflag, 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 sharedset_is_org_adminprimitive so the membership role, flag, and group can never diverge.Shared primitive
Extracted
sync_admins_group_tx; bothset_is_org_admin(one identity — used by invite-accept) and the new atomicset_org_member_admin(a whole membership — used by the endpoint) route through it. No divergent admin-granting logic.Dashboard (vertical integration)
members/+page.sveltegains a Role column/badge and an admin-only Make admin / Remove admin control in the member drawer.is_org_adminis now exposed onIdentityResponse; the page reads viewer admin state from the layout to gate the control.Testing
cargo test -p overslash-api— neworg_member_role.rs(promote → passesAdminAcl; demote → loses it; last-admin refusal incl. self; non-admin 403; invalid role) all green; extendedoverslash_managed_signin.rsto assert an invited admin passes anAdminAcl-gated request. Adjacentidentity_remove_user.rsstill green.cargo clippyclean,cargo fmtclean,svelte-check0 errors, strict dashboard build passes./vet— no issues.Screenshots
Members page admin control — to be attached via the release-asset convention.
Notes
8ef58(domain-JIT): the sharedset_is_org_admincall is the single place the invite/JIT path grants admin.🤖 Generated with Claude Code