diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api/admin.py')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/admin.py | 42 |
1 files changed, 42 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/admin.py b/packages/meshbay-hub/src/meshbay_hub/api/admin.py index b4b2f4f..381378c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/admin.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/admin.py @@ -329,6 +329,25 @@ async def admin_patch_user( raise HTTPException( status_code=422, detail="status must be active, suspended, or revoked") + # Revoking is not a status write. It is signed and broadcast to every + # node so the account is refused there at once; PATCH would change only + # the hub row and leave the nodes unaware — a "revoked" that is not the + # revoked the design promises (§7.5). One door, and it broadcasts. + # (A moderator was already refused above by `privileged`; this is the + # admin, who is told the right door rather than given a broken one.) + if body.status == "revoked": + raise HTTPException( + status_code=400, + detail="Revoke through POST /v1/admin/revoke — it signs the " + "revocation and broadcasts it to every node.") + # Leaving `revoked` undoes a signed, node-enforced action, so it is an + # admin's call, never a moderator's: the nodes still deny this account + # from the broadcast, and a moderator flipping the hub row back to + # active would only disagree with them. + if user.status == "revoked" and not user_is_admin(current_user): + raise HTTPException( + status_code=403, + detail="Only an admin can change a revoked account.") user.status = body.status log.info("User %s status changed to %s by %s", user.username, body.status, current_user.username) @@ -489,6 +508,29 @@ async def admin_patch_group( raise HTTPException( status_code=422, detail="status must be active, suspended, or revoked") + # Suspend/unsuspend is a moderator's reversible, hub-only lever (§7.5). + # Revoke is neither: it is signed and broadcast to every node, and it is + # an administrative act — the same line `admin_patch_user` draws. Two + # gaps used to sit here: a moderator could set `revoked`, and a + # `revoked` set through this PATCH was never broadcast, so it behaved + # like `suspended` on nodes while claiming to be the signed, enforced + # state. Revoke has one door, `POST /v1/admin/revoke`, and it broadcasts. + if body.status == "revoked": + if not user_is_admin(current_user): + raise HTTPException( + status_code=403, + detail="Revoking a group requires admin rights.") + raise HTTPException( + status_code=400, + detail="Revoke a group through POST /v1/admin/revoke — it signs " + "the revocation and broadcasts it to every node.") + # Leaving `revoked` undoes that broadcast and is an admin's call: the + # nodes still enforce the revocation, and a moderator flipping the hub + # row back would only disagree with them. + if group.status == "revoked" and not user_is_admin(current_user): + raise HTTPException( + status_code=403, + detail="Only an admin can change a revoked group.") group.status = body.status log.info("Group %s status changed to %s by %s", group.name, body.status, current_user.username) |