aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/api/admin.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api/admin.py')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/admin.py42
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)