aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_sharing_is_local_only.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-02 11:54:56 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-02 11:54:56 +0200
commitd7b7f1049d95e45e6316ac419cb434088c15bd5e (patch)
tree77da46e2fe3cb70af5fb2eebcbb1d69dad410b88 /packages/meshbay-node/tests/test_sharing_is_local_only.py
parent56a8cf9167e8c7b0f2df15afed88031608adf782 (diff)
parent754387590fa1754436b4648f969915888c6f6c9e (diff)
downloadmeshbay-d7b7f1049d95e45e6316ac419cb434088c15bd5e.tar.gz
Merge branch 'main' of meshbay.org:meshbayHEADmain
Diffstat (limited to 'packages/meshbay-node/tests/test_sharing_is_local_only.py')
-rw-r--r--packages/meshbay-node/tests/test_sharing_is_local_only.py109
1 files changed, 109 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_sharing_is_local_only.py b/packages/meshbay-node/tests/test_sharing_is_local_only.py
new file mode 100644
index 0000000..ac8bebb
--- /dev/null
+++ b/packages/meshbay-node/tests/test_sharing_is_local_only.py
@@ -0,0 +1,109 @@
+"""
+What of the operator's disk is shared, and who may write there, is decided on
+the node's own machine — never over MNP (MNP 6.0).
+
+A signed operation proves that the operator's key signed, not that the operator
+meant it: in a browser the key is driven by code the hub serves, and in the
+desktop application by a renderer that parses content from nodes. `root_add`,
+`root_update` and `group_attach` let either of them share any folder on the
+machine, or open one to writes, from anywhere. They are gone; the loopback API
+(the desktop application) and the CLI remain.
+
+And the consequence that has to hold for the operator's list to stay true: a
+flag changed through the loopback API reaches every connected page, which the
+MNP ack used to do.
+"""
+
+from dataclasses import asdict
+from pathlib import Path
+from types import SimpleNamespace
+
+import pytest
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+from meshbay_common import adminop
+from meshbay_common.protocol import MNP
+from meshbay_node import ops
+from meshbay_node.config import GroupConfig, RootSpec
+from meshbay_node.indexer.indexer import DirectoryIndexer
+from meshbay_node.roots import RootSet
+from meshbay_node.transport.webrtc.admin import _ADMIN_EXECUTORS
+from meshbay_node.transport.webrtc.dispatch import _HANDLERS
+from meshbay_node.transport.webrtc_server import WebRTCPeerSession
+from test_admin_challenge_bounds import _PC, _Channel
+
+GROUP = "g" * 32
+GONE = ("root_add", "root_update", "group_attach")
+
+
+@pytest.mark.parametrize("mtype", GONE)
+def test_no_message_widens_what_the_node_shares(mtype):
+ assert mtype not in _HANDLERS, f"{mtype} is dispatched again"
+ assert mtype not in _ADMIN_EXECUTORS, f"{mtype} can be executed again"
+ assert mtype not in vars(MNP).values(), f"{mtype} is back in the protocol"
+ assert mtype not in vars(adminop).values(), f"{mtype} is a signed op again"
+
+
+@pytest.mark.parametrize("mtype", GONE)
+def test_an_older_client_asking_is_issued_nothing_to_sign(mtype):
+ """A 5.x client still sends these. Nothing it sends may come back as a
+ challenge — a challenge is what a compromised page needs signed."""
+ ctx = {"sk_node": Ed25519PrivateKey.from_private_bytes(b"\x01" * 32),
+ "groups": {GROUP: {}}, "has_admin_authority": True}
+ s = WebRTCPeerSession(_PC(), ctx, peer_id="peer")
+ s._channel = _Channel()
+ s._audit = lambda *a, **k: None
+ s._user_id, s._group_id = "operator", GROUP
+
+ s._dispatch_message({"type": mtype, "group_id": GROUP, "path": "/home/someone",
+ "shared_dir": "/home/someone", "name": "x",
+ "root_name": "x", "writable": True})
+
+ assert s._admin_ops == {}
+ assert not [m for m in s._channel.sent if m.get("type") == "admin_challenge"]
+
+
+def _state(tmp_path: Path, indexer_roots: RootSet | None = None):
+ (tmp_path / "media").mkdir()
+ cfg = GroupConfig(id=GROUP, name="plop",
+ roots=[RootSpec(path=str(tmp_path / "media"), name="media")])
+ conf = tmp_path / "node.toml"
+ conf.write_text(
+ f'[[groups]]\nid = "{GROUP}"\nname = "plop"\n\n'
+ f' [[groups.roots]]\n path = "{(tmp_path / "media").as_posix()}"\n'
+ f' name = "media"\n', encoding="utf-8")
+ live = RootSet.build([asdict(r) for r in cfg.roots])
+ pushed: list[list[dict]] = []
+
+ async def on_change(indexer):
+ pushed.append(indexer.index.roots)
+
+ indexer = DirectoryIndexer(indexer_roots or live, GROUP,
+ Ed25519PrivateKey.generate(), None,
+ on_change=on_change)
+ state = {"config": SimpleNamespace(groups=[cfg]), "config_path": str(conf),
+ "groups_ctx": {GROUP: {"roots": live}},
+ "indexers": {GROUP: indexer}}
+ return state, pushed
+
+
+async def test_a_flag_changed_on_the_node_reaches_every_open_page(tmp_path):
+ state, pushed = _state(tmp_path)
+
+ await ops.update_root(state, GROUP, "media", writable=True)
+
+ assert pushed, "nothing was pushed — open pages keep the old flag"
+ assert pushed[-1][0]["writable"] is True
+
+
+async def test_the_pushed_table_is_right_when_the_indexer_holds_its_own_set(tmp_path):
+ """The indexer and the group context normally share one RootSet; when they
+ do not, the table peers receive is the indexer's, and must carry the flag."""
+ (tmp_path / "media").mkdir()
+ own = RootSet.build([{"path": str(tmp_path / "media"), "name": "media"}])
+ (tmp_path / "media").rmdir()
+ state, pushed = _state(tmp_path, indexer_roots=own)
+
+ await ops.update_root(state, GROUP, "media", removable=True)
+
+ assert pushed[-1][0]["removable"] is True
+ assert state["groups_ctx"][GROUP]["roots"].by_name("media").removable is True