aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_revocation.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-28 02:51:00 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-28 02:51:00 +0200
commitb5b4f188a39fc96c4d32e67151e067b1add6dcfc (patch)
treece0c4ff78044a56c0882d7ba94fbea436f0e83c3 /packages/meshbay-hub/tests/test_revocation.py
parente1f1b65cfac031096e4bae24ccf102ca0dbb86d9 (diff)
downloadmeshbay-b5b4f188a39fc96c4d32e67151e067b1add6dcfc.tar.gz
feat(hub): let a hub admin disable public groups instance-wide
A new General tab in Administration carries one switch, allow_public_groups, stored in a hub_settings key/value table (runtime-editable, unlike hub.toml). Default is on; an absent row means on, so an upgrade changes nothing. Enforcement is server-side on every hub-mediated path, not just the SPA: - create_group refuses visibility=public (403), staff included - list_public_groups the directory returns nothing (local + federated) - join_group open-joining a public group is refused - group_online_nodes a non-member of a public group is handed no node - signaling.webrtc_offer drops the "node hosts an open group" fallback - federation.export_directory advertises nothing to peer hubs The switch is read live, so flipping it back restores every path. Existing members of a group that predates the switch keep their membership row and their access — this is plan A, not a purge. GET /v1/hub/info exposes the flag (unauthenticated) so the create-group form and the sidebar's "Public groups" link render correctly. Also in the admin Groups tab: a Revoke action beside Suspend. Suspend is the reversible hub flag; Revoke calls POST /v1/admin/revoke, which sets status=revoked and broadcasts a signed revocation every node enforces (denylist + dropped live sessions). It is confirm-guarded and names the group. And a message fix the revoke work surfaced: group_online_nodes, join_group and webrtc_offer answered "Group is suspended" for any non-active status. They now report the real state, so a member of a revoked group is told "Group is revoked" rather than something reversible-sounding. Tests: test_public_groups_toggle.py (10) covers the switch end to end and the five enforcement paths; test_revocation.py gains the status-message assertion. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018gKJ85aZyvEwarXMFzFEwi
Diffstat (limited to 'packages/meshbay-hub/tests/test_revocation.py')
-rw-r--r--packages/meshbay-hub/tests/test_revocation.py34
1 files changed, 34 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_revocation.py b/packages/meshbay-hub/tests/test_revocation.py
index 494d77d..d1147e1 100644
--- a/packages/meshbay-hub/tests/test_revocation.py
+++ b/packages/meshbay-hub/tests/test_revocation.py
@@ -121,3 +121,37 @@ async def test_revoke_group(client):
}, headers=hdrs)
assert r.status_code == 200
assert r.json()["status"] == "revoked"
+
+
+@pytest.mark.asyncio
+async def test_group_status_message_names_the_real_state(client):
+ """A member of a revoked group must not be told it was merely 'suspended'.
+
+ `GET /v1/groups/{id}/nodes` and `POST /join` used to answer "Group is
+ suspended" for any non-active status. Suspend is the reversible hub flag;
+ revoke is a signed instruction every node enforces. The client shows this
+ string verbatim, so it has to be the truth.
+ """
+ sk_ed = Ed25519PrivateKey.generate()
+ sk_x = X25519PrivateKey.generate()
+ admin_token, _ = await _register_and_login(
+ client, "admin_msg",
+ pk_to_b64(sk_ed.public_key()), pk_to_b64(sk_x.public_key()))
+ set_admin_usernames(["admin_msg"])
+ hdrs = {"Authorization": f"Bearer {admin_token}"}
+
+ gid = (await client.post("/v1/groups", json={"name": "state-msg"},
+ headers=hdrs)).json()["group_id"]
+
+ # Suspend → the message says suspended.
+ await client.patch(f"/v1/admin/groups/{gid}", json={"status": "suspended"},
+ headers=hdrs)
+ r = await client.get(f"/v1/groups/{gid}/nodes", headers=hdrs)
+ assert r.status_code == 403 and r.json()["detail"] == "Group is suspended"
+
+ # Revoke → the message says revoked, not suspended.
+ await client.post("/v1/admin/revoke",
+ json={"target": "group", "target_id": gid, "reason": "x"},
+ headers=hdrs)
+ r = await client.get(f"/v1/groups/{gid}/nodes", headers=hdrs)
+ assert r.status_code == 403 and r.json()["detail"] == "Group is revoked"