diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-30 11:49:56 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-30 11:49:56 +0200 |
| commit | d3ad243c4ae3a273f623bd5fc631e3266aa4d0e4 (patch) | |
| tree | 95ff1252c80a71e93c5098822d31b835572d8b52 /packages/meshbay-hub/tests/test_mnp_token.py | |
| parent | 69554fac7eba6eef7eb8a1c0111c5b92e7f21256 (diff) | |
| download | meshbay-d3ad243c4ae3a273f623bd5fc631e3266aa4d0e4.tar.gz | |
fix: only the owner decides who hosts a group, and nobody is made a member unasked
- hub: a node may host a group only if its account owns it or the owner
approved that node (new `group_hosts`). Membership was the ceiling, and
every member holds the group key, so any member's node could register as a
host and be the one clients kept. A node claiming a group it may not host
is recorded as a request; the owner is notified once and approves or
refuses it (GET/POST/DELETE /v1/groups/{id}/hosts[/{node_id}]), which
takes effect on a connected node at once.
- hub: an owner adding a username creates an invitation (new
`group_invitations`), accepted or declined by the invitee
(/v1/groups/invitations, /{id}/invitation/accept|decline). Until then the
group is not listed, not dialled, not searched and not in any token.
Invitation links, open joins and group creation still make members
directly: they are the account's own act.
- hub: the MNP token names only the group it is minted for (group_id is now
required), so a node operator no longer learns a member's other groups.
- SPA: invitations on the home page; invited people and host requests in
the group's settings; the transport sends group_id. Ten catalogues.
- Browser probes for both screens, run in Chrome and Firefox.
- Design §5.2, §7.2, §7.3, AV32, AV33; protocol §6.3; user guide.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/tests/test_mnp_token.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_mnp_token.py | 58 |
1 files changed, 52 insertions, 6 deletions
diff --git a/packages/meshbay-hub/tests/test_mnp_token.py b/packages/meshbay-hub/tests/test_mnp_token.py index 3aa3115..ef6423d 100644 --- a/packages/meshbay-hub/tests/test_mnp_token.py +++ b/packages/meshbay-hub/tests/test_mnp_token.py @@ -8,9 +8,7 @@ these tests pin that it authorises to a node and is refused by the hub API. """ import pytest - from meshbay_common.handshake import HandshakeError, authorize_token -from meshbay_common.tokens import MNP_AUD async def _session_token(client, username="mnp_user_test"): @@ -21,6 +19,12 @@ async def _session_token(client, username="mnp_user_test"): return r.json()["access_token"] +async def _own_group(client, tok, name="g"): + return (await client.post("/v1/groups", headers={"Authorization": f"Bearer {tok}"}, + json={"name": name, "visibility": "private", + "join_policy": "invite"})).json()["group_id"] + + @pytest.mark.asyncio async def test_mnp_token_endpoint_needs_a_session(client): # No Authorization header at all — FastAPI rejects the required header (422), @@ -32,7 +36,8 @@ async def test_mnp_token_endpoint_needs_a_session(client): @pytest.mark.asyncio async def test_mnp_token_is_minted_for_a_member(client): tok = await _session_token(client) - r = await client.post("/v1/nodes/mnp-token", + gid = await _own_group(client, tok) + r = await client.post("/v1/nodes/mnp-token", json={"group_id": gid}, headers={"Authorization": f"Bearer {tok}"}) assert r.status_code == 200 assert r.json().get("mnp_token") @@ -42,7 +47,8 @@ async def test_mnp_token_is_minted_for_a_member(client): async def test_mnp_token_is_refused_at_the_hub_api(client): """The whole point: the credential a node receives opens nothing at the hub.""" tok = await _session_token(client, "mnp_api_test") - mnp = (await client.post("/v1/nodes/mnp-token", + gid = await _own_group(client, tok) + mnp = (await client.post("/v1/nodes/mnp-token", json={"group_id": gid}, headers={"Authorization": f"Bearer {tok}"})).json()["mnp_token"] # Presenting it to a hub endpoint fails. r = await client.get("/v1/users/me", headers={"Authorization": f"Bearer {mnp}"}) @@ -60,7 +66,8 @@ async def test_a_session_token_is_refused_by_a_node_but_the_mnp_token_is_not(cli H = {"Authorization": f"Bearer {tok}"} gid = (await client.post("/v1/groups", headers=H, json={ "name": "g", "visibility": "private", "join_policy": "invite"})).json()["group_id"] - mnp = (await client.post("/v1/nodes/mnp-token", headers=H)).json()["mnp_token"] + mnp = (await client.post("/v1/nodes/mnp-token", headers=H, + json={"group_id": gid})).json()["mnp_token"] pk = hub_public_key_pem() # The session token is refused by the node handshake (wrong audience). @@ -83,10 +90,49 @@ async def test_the_mnp_token_is_bound_to_the_node_it_names(client): "name": "g", "visibility": "private", "join_policy": "invite"})).json()["group_id"] # A token bound to node A's key. mnp = (await client.post("/v1/nodes/mnp-token", headers=H, - json={"node_pk": "node-A-pk"})).json()["mnp_token"] + json={"node_pk": "node-A-pk", "group_id": gid})).json()["mnp_token"] pk = hub_public_key_pem() # Node B refuses it; node A accepts it. with pytest.raises(HandshakeError, match="this node"): authorize_token(mnp, pk, group_id=gid, node_pk_b64="node-B-pk") peer = authorize_token(mnp, pk, group_id=gid, node_pk_b64="node-A-pk") assert peer.group_id == gid + + +@pytest.mark.asyncio +async def test_the_mnp_token_names_only_the_group_asked_for(client): + """It is handed to that group's node operator, who has no business learning + every other group the member belongs to.""" + import jwt as _jwt + + tok = await _session_token(client, "mnp_scope_test") + H = {"Authorization": f"Bearer {tok}"} + one = await _own_group(client, tok, "one") + await _own_group(client, tok, "two") + await _own_group(client, tok, "three") + mnp = (await client.post("/v1/nodes/mnp-token", headers=H, + json={"group_id": one})).json()["mnp_token"] + claims = _jwt.decode(mnp, options={"verify_signature": False}) + assert claims["groups"] == [one] + + +@pytest.mark.asyncio +async def test_no_group_is_named_for_a_non_member(client): + from meshbay_hub.auth import hub_public_key_pem + + owner = await _session_token(client, "mnp_owner_test") + gid = await _own_group(client, owner) + stranger = await _session_token(client, "mnp_stranger") + mnp = (await client.post("/v1/nodes/mnp-token", json={"group_id": gid}, + headers={"Authorization": f"Bearer {stranger}"})).json()["mnp_token"] + with pytest.raises(HandshakeError) as refused: + authorize_token(mnp, hub_public_key_pem(), group_id=gid) + assert refused.value.code == "not_a_member" + + +@pytest.mark.asyncio +async def test_a_token_must_name_its_group(client): + tok = await _session_token(client, "mnp_nogroup_test") + r = await client.post("/v1/nodes/mnp-token", json={}, + headers={"Authorization": f"Bearer {tok}"}) + assert r.status_code == 422 |