""" Multi-group isolation test: two groups on one QUIC server. Verifies that: - A user in group-a can fetch files from group-a - A user in group-a is rejected when requesting group-b - A user in both groups can access both """ import time import jwt import pytest from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from meshbay_common.crypto import generate_gek, pk_to_b64 from meshbay_node.indexer import DirectoryIndexer from meshbay_node.transport.quic_client import QuicChunkClient from meshbay_node.transport.quic_server import QuicChunkServer from conftest import one_root @pytest.fixture def sk_node(): return Ed25519PrivateKey.generate() @pytest.fixture def sk_hub(): return Ed25519PrivateKey.generate() @pytest.fixture def gek_a(): return generate_gek() @pytest.fixture def gek_b(): return generate_gek() @pytest.fixture def dir_a(tmp_path): d = tmp_path / "group_a" d.mkdir() (d / "file_a.txt").write_bytes(b"content from group A " * 100) return d @pytest.fixture def dir_b(tmp_path): d = tmp_path / "group_b" d.mkdir() (d / "file_b.txt").write_bytes(b"content from group B " * 100) return d def make_jwt(sk_hub, pk_node_b64, groups, user_id="user-001", ttl=3600): sk_pem = sk_hub.private_bytes( serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption(), ) now = int(time.time()) return jwt.encode({ "iss": "test-hub", "sub": user_id, "pk_user": pk_node_b64, "hub_id": "test-hub", "jti": "test-jti", "iat": now, "exp": now + ttl, "groups": groups, }, sk_pem, algorithm="EdDSA") @pytest.fixture async def multi_group_server(sk_node, sk_hub, gek_a, gek_b, dir_a, dir_b, tmp_path): hub_pk_pem = sk_hub.public_key().public_bytes( serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo) indexer_a = DirectoryIndexer(roots=one_root(dir_a), group_id="group-a", sk_node=sk_node, gek=gek_a) await indexer_a.initial_scan() indexer_b = DirectoryIndexer(roots=one_root(dir_b), group_id="group-b", sk_node=sk_node, gek=gek_b) await indexer_b.initial_scan() # RootSet, not a bare Path — what the daemon actually puts in a group context. # This held a Path until 2026-09-03 and nothing noticed: the QUIC index handler # only called `index.serialize()`, and `entry_abs_path` fell through # `Path.resolve(strict=...)`, reading the virtual path as a truthy flag and # returning the right file by accident. groups = { "group-a": {"gek": gek_a, "roots": one_root(dir_a), "index": indexer_a.index}, "group-b": {"gek": gek_b, "roots": one_root(dir_b), "index": indexer_b.index}, } cert_path = tmp_path / "node.crt" key_path = tmp_path / "node.key" server = QuicChunkServer( sk_node=sk_node, hub_pk_pem=hub_pk_pem, gek=gek_a, roots=one_root(dir_a), index=indexer_a.index, host="127.0.0.1", port=19200, cert_path=cert_path, key_path=key_path, groups=groups, ) await server.start() yield server, indexer_a, indexer_b await server.stop() @pytest.mark.asyncio async def test_user_can_access_own_group( multi_group_server, sk_node, sk_hub, gek_a, ): """User in group-a can fetch index and chunks from group-a.""" server, indexer_a, _ = multi_group_server token = make_jwt(sk_hub, pk_to_b64(sk_node.public_key()), groups=["group-a"]) async with QuicChunkClient( host="127.0.0.1", port=19200, jwt_token=token, gek=gek_a, pk_node_b64=pk_to_b64(sk_node.public_key()), group_id="group-a", ) as client: msg = await client.fetch_index() assert len(msg["entries"]) == 1 entry = msg["entries"][0] assert entry["name"] == "file_a.txt" chunk = await client.fetch_chunk(entry["id"], chunk_index=0) assert chunk == b"content from group A " * 100 @pytest.mark.asyncio async def test_user_rejected_from_other_group( multi_group_server, sk_node, sk_hub, gek_b, ): """User in group-a only is rejected when requesting group-b.""" server, _, _ = multi_group_server token = make_jwt(sk_hub, pk_to_b64(sk_node.public_key()), groups=["group-a"]) with pytest.raises(ConnectionError, match="rejected"): async with QuicChunkClient( host="127.0.0.1", port=19200, jwt_token=token, gek=gek_b, pk_node_b64=pk_to_b64(sk_node.public_key()), group_id="group-b", ) as client: await client.fetch_index() @pytest.mark.asyncio async def test_dual_group_user_accesses_both( multi_group_server, sk_node, sk_hub, gek_a, gek_b, ): """User in both groups can access either group's files.""" server, indexer_a, indexer_b = multi_group_server token = make_jwt(sk_hub, pk_to_b64(sk_node.public_key()), groups=["group-a", "group-b"]) async with QuicChunkClient( host="127.0.0.1", port=19200, jwt_token=token, gek=gek_a, pk_node_b64=pk_to_b64(sk_node.public_key()), group_id="group-a", ) as client_a: msg_a = await client_a.fetch_index() assert msg_a["entries"][0]["name"] == "file_a.txt" async with QuicChunkClient( host="127.0.0.1", port=19200, jwt_token=token, gek=gek_b, pk_node_b64=pk_to_b64(sk_node.public_key()), group_id="group-b", ) as client_b: msg_b = await client_b.fetch_index() assert msg_b["entries"][0]["name"] == "file_b.txt"