diff options
Diffstat (limited to 'packages/meshbay-node/tests/test_roster_pairing.py')
| -rw-r--r-- | packages/meshbay-node/tests/test_roster_pairing.py | 41 |
1 files changed, 27 insertions, 14 deletions
diff --git a/packages/meshbay-node/tests/test_roster_pairing.py b/packages/meshbay-node/tests/test_roster_pairing.py index 435cc76..9e45dbc 100644 --- a/packages/meshbay-node/tests/test_roster_pairing.py +++ b/packages/meshbay-node/tests/test_roster_pairing.py @@ -22,6 +22,7 @@ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey from meshbay_common.crypto import generate_gek, pk_to_b64, unwrap_gek_aes from meshbay_common.join import ROLE_MEMBER, ROLE_OPERATOR, join_transcript from meshbay_node.indexer.group_index import GroupIndex +from conftest import one_root from meshbay_node.roster import Roster, hash_code, normalize_code from meshbay_node.transport.webrtc_server import WebRTCPeerSession @@ -65,7 +66,7 @@ def _session(tmp_path: Path, roster, user_id: str = "grenet", session = WebRTCPeerSession.__new__(WebRTCPeerSession) session._ctx = { - "shared_root": shared_root, + "roots": one_root(shared_root), "index": index, "sk_node": index.sk_node, "roster": roster, @@ -74,7 +75,7 @@ def _session(tmp_path: Path, roster, user_id: str = "grenet", session._ctx["groups"] = { group_id: { "gek": gek, - "shared_root": shared_root, + "roots": one_root(shared_root), "index": index, "join_policy": join_policy, }, @@ -599,7 +600,7 @@ async def test_revoke_endpoint_stops_authorization(tmp_path, roster): resp = client.post(f"/api/members/bob/revoke?group_id={GROUP}&t=tok") assert resp.status_code == 200 - assert "gek-init" in resp.json()["reminder"], ( + assert "gek rotate" in resp.json()["reminder"], ( "revocation must remind the operator to rotate the key they still hold") assert not await roster.is_authorized(GROUP, "bob") @@ -823,7 +824,7 @@ async def test_a_directory_with_anything_in_it_is_refused(tmp_path, roster): full.mkdir() (full / "keep.txt").write_text("still here") - session._do_dir_delete({"dir": "full"}) + session._do_dir_delete({"dir": "shared/full"}) assert _last(session).get("detail") == "Directory is not empty" assert full.exists() and (full / "keep.txt").exists() @@ -835,15 +836,23 @@ async def test_no_challenge_is_issued_without_an_operator(tmp_path, roster): session._ctx["has_admin_authority"] = False (tmp_path / "shared" / "empty").mkdir() - session._do_dir_delete({"dir": "empty"}) + session._do_dir_delete({"dir": "shared/empty"}) assert _last(session).get("detail") == "No authorized key for deletion" assert (tmp_path / "shared" / "empty").exists() -async def test_the_shared_root_itself_is_not_a_target(tmp_path, roster): +async def test_a_root_itself_is_not_a_target(tmp_path, roster): + """ + Neither the virtual root nor a root directory can be removed this way. + + Removing a root is a configuration change: doing it through a file operation + would leave the group config naming a directory nobody can reach. And the + virtual root is not a directory on anyone's disk at all — it belongs to no + volume. + """ session = await _dir_session(tmp_path, roster) - for attempt in ("", ".", "/", "../shared"): + for attempt in ("", ".", "/", "../shared", "shared", "shared/", "SHARED"): session._do_dir_delete({"dir": attempt}) assert _last(session).get("type") == "error", f"{attempt!r} was accepted" assert (tmp_path / "shared").is_dir() @@ -854,7 +863,11 @@ async def test_escaping_the_shared_root_is_refused(tmp_path, roster): outside = tmp_path / "outside" outside.mkdir() - for attempt in ("../outside", "../../outside", "sub/../../outside"): + # Both shapes: a path that names no root at all, and one that starts inside + # a real root and then climbs out of it. + for attempt in ("../outside", "../../outside", "sub/../../outside", + "shared/../outside", "shared/../../outside", + "shared/sub/../../outside"): session._do_dir_delete({"dir": attempt}) assert _last(session).get("type") == "error", f"{attempt!r} was accepted" assert outside.is_dir(), "a path leaving the shared root removed a directory" @@ -871,19 +884,19 @@ async def test_an_empty_directory_needs_a_signature_and_then_goes(tmp_path, rost session = await _dir_session(tmp_path, roster) (tmp_path / "shared" / "gone").mkdir() - session._do_dir_delete({"dir": "gone"}) + session._do_dir_delete({"dir": "shared/gone"}) challenge = _last(session) assert challenge["type"] == "admin_challenge" assert challenge["op"] == "dir_delete" - assert challenge["subject"] == "gone" + assert challenge["subject"] == "shared/gone" transcript = admin_transcript( op="dir_delete", node_pk_b64=session._node_pk_b64(), group_id="g1", - subject="gone", nonce=base64.b64decode(challenge["nonce"]), + subject="shared/gone", nonce=base64.b64decode(challenge["nonce"]), ts=challenge["ts"]) await session._admin_exec_dir_delete( session._admin_ops.pop(challenge["op_id"]) if session._admin_ops - else {"op": "dir_delete", "subject": "gone"}, + else {"op": "dir_delete", "subject": "shared/gone"}, transcript, sk_ed.sign(transcript)) assert _last(session)["type"] == "dir_delete_ack" @@ -906,9 +919,9 @@ async def test_someone_elses_signature_does_not_remove_it(tmp_path, roster): transcript = admin_transcript( op="dir_delete", node_pk_b64=session._node_pk_b64(), group_id="g1", - subject="theirs", nonce=b"\x22" * 32, ts=int(time.time())) + subject="shared/theirs", nonce=b"\x22" * 32, ts=int(time.time())) await session._admin_exec_dir_delete( - {"op": "dir_delete", "subject": "theirs"}, + {"op": "dir_delete", "subject": "shared/theirs"}, transcript, sk_member.sign(transcript)) assert _last(session).get("detail") == "Signature verification failed" |