diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-02 10:20:09 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-02 10:20:09 +0200 |
| commit | e4f61771131be635b9e81a19203a00707b4b19df (patch) | |
| tree | d80e4edafbeade3c27137e6753140e6585a26b9b /packages/meshbay-node/tests | |
| parent | e941cc4c39c38a12220153ea572bd4c7bb92fde0 (diff) | |
| download | meshbay-e4f61771131be635b9e81a19203a00707b4b19df.tar.gz | |
feat(mnp): sharing a folder is decided on the node's machine only (MNP 6.0)
root_add, root_update and group_attach leave MNP: adding a directory and
switching writable/removable go through the loopback API (native dialog in
the desktop app) or the CLI. The operator's Settings tab still lists the
roots from any browser, read-only. The desktop app refuses to sign those
ops; a loopback flag change now reaches open pages (publish_roots).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/tests')
| -rw-r--r-- | packages/meshbay-node/tests/golden/dispatch.json | 1237 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/test_admin_challenge_bounds.py | 45 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/test_node_status.py | 58 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/test_root_writable_policy.py | 38 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/test_sharing_is_local_only.py | 109 |
5 files changed, 177 insertions, 1310 deletions
diff --git a/packages/meshbay-node/tests/golden/dispatch.json b/packages/meshbay-node/tests/golden/dispatch.json index a7bb543..345b077 100644 --- a/packages/meshbay-node/tests/golden/dispatch.json +++ b/packages/meshbay-node/tests/golden/dispatch.json @@ -84,14 +84,6 @@ "_admin_exec_gek_rotate" ] }, - "group_attach": { - "audit": [], - "log": [], - "sent": [], - "spawned": [ - "_admin_exec_group_attach" - ] - }, "group_detach": { "audit": [], "log": [], @@ -160,14 +152,6 @@ ], "spawned": [] }, - "root_add": { - "audit": [], - "log": [], - "sent": [], - "spawned": [ - "_admin_exec_root_add" - ] - }, "root_eject": { "audit": [], "log": [], @@ -192,14 +176,6 @@ "_admin_exec_root_remove" ] }, - "root_update": { - "audit": [], - "log": [], - "sent": [], - "spawned": [ - "_admin_exec_root_update" - ] - }, "search_listed": { "audit": [], "log": [], @@ -2068,7 +2044,7 @@ "subject": "x", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -2391,7 +2367,7 @@ "subject": "gggggggggggggggggggggggggggggggg", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -2410,7 +2386,7 @@ "subject": "['x']", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -2429,7 +2405,7 @@ "subject": "7", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -2448,7 +2424,7 @@ "subject": "x", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -2718,7 +2694,7 @@ "messages": [], "req_id": 4242, "type": "chat_hist_resp", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -2732,7 +2708,7 @@ "messages": [], "req_id": 4242, "type": "chat_hist_resp", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -2746,7 +2722,7 @@ "messages": [], "req_id": 4242, "type": "chat_hist_resp", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -2760,7 +2736,7 @@ "messages": [], "req_id": 4242, "type": "chat_hist_resp", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -2774,7 +2750,7 @@ "messages": [], "req_id": 4242, "type": "chat_hist_resp", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -2788,7 +2764,7 @@ "messages": [], "req_id": 4242, "type": "chat_hist_resp", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -2802,7 +2778,7 @@ "messages": [], "req_id": 4242, "type": "chat_hist_resp", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -2816,7 +2792,7 @@ "messages": [], "req_id": 4242, "type": "chat_hist_resp", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -8535,7 +8511,7 @@ "subject": "gggggggggggggggggggggggggggggggg", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -8554,7 +8530,7 @@ "subject": "['x']", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -8573,7 +8549,7 @@ "subject": "7", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -8592,7 +8568,7 @@ "subject": "x", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -8757,379 +8733,6 @@ "sent": [], "spawned": [] }, - "group_attach | challenged | bare": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach | challenged | lists": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach | challenged | numbers": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach | challenged | strings": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach | fresh | bare": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach | fresh | lists": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach | fresh | numbers": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach | fresh | strings": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach | member | bare": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Missing name or shared_dir", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach | member | lists": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "No authorized key for this", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach | member | numbers": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "No authorized key for this", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach | member | strings": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "No authorized key for this", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach | operator | bare": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Missing name or shared_dir", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach | operator | lists": { - "audit": [], - "log": [], - "sent": [ - { - "group_id": "", - "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=", - "nonce": "<volatile>", - "op": "group_attach", - "op_id": "<volatile>", - "req_id": 4242, - "subject": "{\"name\":\"['x']\",\"shared_dir\":\"['x']\",\"writable\":true}", - "ts": "<volatile>", - "type": "admin_challenge", - "v": "5.0" - } - ], - "spawned": [] - }, - "group_attach | operator | numbers": { - "audit": [], - "log": [], - "sent": [ - { - "group_id": "", - "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=", - "nonce": "<volatile>", - "op": "group_attach", - "op_id": "<volatile>", - "req_id": 4242, - "subject": "{\"name\":\"7\",\"shared_dir\":\"7\",\"writable\":true}", - "ts": "<volatile>", - "type": "admin_challenge", - "v": "5.0" - } - ], - "spawned": [] - }, - "group_attach | operator | strings": { - "audit": [], - "log": [], - "sent": [ - { - "group_id": "", - "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=", - "nonce": "<volatile>", - "op": "group_attach", - "op_id": "<volatile>", - "req_id": 4242, - "subject": "{\"name\":\"x\",\"shared_dir\":\"x\",\"writable\":true}", - "ts": "<volatile>", - "type": "admin_challenge", - "v": "5.0" - } - ], - "spawned": [] - }, - "group_attach_ack | challenged | bare": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach_ack | challenged | lists": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach_ack | challenged | numbers": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach_ack | challenged | strings": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach_ack | fresh | bare": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach_ack | fresh | lists": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach_ack | fresh | numbers": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach_ack | fresh | strings": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "group_attach_ack | member | bare": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "group_attach_ack | member | lists": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "group_attach_ack | member | numbers": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "group_attach_ack | member | strings": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "group_attach_ack | operator | bare": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "group_attach_ack | operator | lists": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "group_attach_ack | operator | numbers": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "group_attach_ack | operator | strings": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, "group_detach | challenged | bare": { "audit": [], "log": [], @@ -9300,7 +8903,7 @@ "subject": "['x']", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -9319,7 +8922,7 @@ "subject": "7", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -9338,7 +8941,7 @@ "subject": "x", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -11641,7 +11244,7 @@ "subject": "link:gggggggggggggggggggggggggggggggg", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -13740,7 +13343,7 @@ "subject": "['x']", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -13759,7 +13362,7 @@ "subject": "7", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -13778,7 +13381,7 @@ "subject": "x", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -14113,7 +13716,7 @@ "subject": "['x']", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -14132,7 +13735,7 @@ "subject": "7", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -14151,7 +13754,7 @@ "subject": "x", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -16212,7 +15815,7 @@ "req_id": 4242, "token": null, "type": "pong", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -16227,7 +15830,7 @@ "x" ], "type": "pong", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -16240,7 +15843,7 @@ "req_id": 4242, "token": 7, "type": "pong", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -16253,7 +15856,7 @@ "req_id": 4242, "token": "x", "type": "pong", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -16266,7 +15869,7 @@ "req_id": 4242, "token": null, "type": "pong", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -16281,7 +15884,7 @@ "x" ], "type": "pong", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -16294,7 +15897,7 @@ "req_id": 4242, "token": 7, "type": "pong", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -16307,7 +15910,7 @@ "req_id": 4242, "token": "x", "type": "pong", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -16472,379 +16075,6 @@ "sent": [], "spawned": [] }, - "root_add | challenged | bare": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add | challenged | lists": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add | challenged | numbers": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add | challenged | strings": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add | fresh | bare": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add | fresh | lists": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add | fresh | numbers": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add | fresh | strings": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add | member | bare": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Missing group_id or path", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add | member | lists": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "No authorized key for this", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add | member | numbers": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "No authorized key for this", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add | member | strings": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "No authorized key for this", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add | operator | bare": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Missing group_id or path", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add | operator | lists": { - "audit": [], - "log": [], - "sent": [ - { - "group_id": "['x']", - "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=", - "nonce": "<volatile>", - "op": "root_add", - "op_id": "<volatile>", - "req_id": 4242, - "subject": "{\"kind\":\"['x']\",\"name\":\"['x']\",\"path\":\"['x']\",\"removable\":true,\"writable\":true}", - "ts": "<volatile>", - "type": "admin_challenge", - "v": "5.0" - } - ], - "spawned": [] - }, - "root_add | operator | numbers": { - "audit": [], - "log": [], - "sent": [ - { - "group_id": "7", - "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=", - "nonce": "<volatile>", - "op": "root_add", - "op_id": "<volatile>", - "req_id": 4242, - "subject": "{\"kind\":\"7\",\"name\":\"7\",\"path\":\"7\",\"removable\":true,\"writable\":true}", - "ts": "<volatile>", - "type": "admin_challenge", - "v": "5.0" - } - ], - "spawned": [] - }, - "root_add | operator | strings": { - "audit": [], - "log": [], - "sent": [ - { - "group_id": "x", - "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=", - "nonce": "<volatile>", - "op": "root_add", - "op_id": "<volatile>", - "req_id": 4242, - "subject": "{\"kind\":\"x\",\"name\":\"x\",\"path\":\"x\",\"removable\":true,\"writable\":true}", - "ts": "<volatile>", - "type": "admin_challenge", - "v": "5.0" - } - ], - "spawned": [] - }, - "root_add_ack | challenged | bare": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add_ack | challenged | lists": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add_ack | challenged | numbers": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add_ack | challenged | strings": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add_ack | fresh | bare": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add_ack | fresh | lists": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add_ack | fresh | numbers": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add_ack | fresh | strings": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_add_ack | member | bare": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "root_add_ack | member | lists": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "root_add_ack | member | numbers": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "root_add_ack | member | strings": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "root_add_ack | operator | bare": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "root_add_ack | operator | lists": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "root_add_ack | operator | numbers": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "root_add_ack | operator | strings": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, "root_eject | challenged | bare": { "audit": [], "log": [], @@ -17015,7 +16245,7 @@ "subject": "['x']", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -17034,7 +16264,7 @@ "subject": "7", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -17053,7 +16283,7 @@ "subject": "x", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -17388,7 +16618,7 @@ "subject": "['x']", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -17407,7 +16637,7 @@ "subject": "7", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -17426,7 +16656,7 @@ "subject": "x", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -17761,7 +16991,7 @@ "subject": "['x']", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -17780,7 +17010,7 @@ "subject": "7", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -17799,7 +17029,7 @@ "subject": "x", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -17964,379 +17194,6 @@ "sent": [], "spawned": [] }, - "root_update | challenged | bare": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update | challenged | lists": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update | challenged | numbers": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update | challenged | strings": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update | fresh | bare": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update | fresh | lists": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update | fresh | numbers": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update | fresh | strings": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update | member | bare": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Missing group_id or root_name", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update | member | lists": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "No authorized key for this", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update | member | numbers": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "No authorized key for this", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update | member | strings": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "No authorized key for this", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update | operator | bare": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Missing group_id or root_name", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update | operator | lists": { - "audit": [], - "log": [], - "sent": [ - { - "group_id": "['x']", - "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=", - "nonce": "<volatile>", - "op": "root_update", - "op_id": "<volatile>", - "req_id": 4242, - "subject": "['x']:rw=on,rem=on", - "ts": "<volatile>", - "type": "admin_challenge", - "v": "5.0" - } - ], - "spawned": [] - }, - "root_update | operator | numbers": { - "audit": [], - "log": [], - "sent": [ - { - "group_id": "7", - "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=", - "nonce": "<volatile>", - "op": "root_update", - "op_id": "<volatile>", - "req_id": 4242, - "subject": "7:rw=on,rem=on", - "ts": "<volatile>", - "type": "admin_challenge", - "v": "5.0" - } - ], - "spawned": [] - }, - "root_update | operator | strings": { - "audit": [], - "log": [], - "sent": [ - { - "group_id": "x", - "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=", - "nonce": "<volatile>", - "op": "root_update", - "op_id": "<volatile>", - "req_id": 4242, - "subject": "x:rw=on,rem=on", - "ts": "<volatile>", - "type": "admin_challenge", - "v": "5.0" - } - ], - "spawned": [] - }, - "root_update_ack | challenged | bare": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update_ack | challenged | lists": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update_ack | challenged | numbers": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update_ack | challenged | strings": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update_ack | fresh | bare": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update_ack | fresh | lists": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update_ack | fresh | numbers": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update_ack | fresh | strings": { - "audit": [], - "log": [], - "sent": [ - { - "detail": "Handshake required", - "req_id": 4242, - "type": "error" - } - ], - "spawned": [] - }, - "root_update_ack | member | bare": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "root_update_ack | member | lists": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "root_update_ack | member | numbers": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "root_update_ack | member | strings": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "root_update_ack | operator | bare": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "root_update_ack | operator | lists": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "root_update_ack | operator | numbers": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, - "root_update_ack | operator | strings": { - "audit": [], - "log": [ - "WARNING Unknown MNP message type on DataChannel: %s" - ], - "sent": [], - "spawned": [] - }, "roster_read | challenged | bare": { "audit": [], "log": [], @@ -21119,7 +19976,7 @@ "subject": "{\"language\":null,\"token\":null}", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -21162,7 +20019,7 @@ "subject": "{\"language\":\"x\",\"token\":\"sha256:2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881\"}", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] @@ -23049,7 +21906,7 @@ "subject": "d=7,u=7", "ts": "<volatile>", "type": "admin_challenge", - "v": "5.0" + "v": "6.0" } ], "spawned": [] diff --git a/packages/meshbay-node/tests/test_admin_challenge_bounds.py b/packages/meshbay-node/tests/test_admin_challenge_bounds.py index fd3b2f1..9dcb750 100644 --- a/packages/meshbay-node/tests/test_admin_challenge_bounds.py +++ b/packages/meshbay-node/tests/test_admin_challenge_bounds.py @@ -3,8 +3,9 @@ What a connection may leave waiting for a signature (docs/MESHBAY_DESIGN.md §13 Anyone authenticated can ask for an admin challenge — the signature is checked later — so a member who never answers must not make the node keep every request. -Measured before the bound: 200 `root_add` of 1 MiB each from a plain member held -200 pending operations and ~400 MiB for the life of the connection. +Measured before the bound: 200 `root_add` (an op since removed) of 1 MiB each +from a plain member held 200 pending operations and ~400 MiB for the life of +the connection. """ import struct @@ -48,23 +49,24 @@ def _member_session(): return s -def _root_add(s, path: str) -> dict: - s._dispatch_message({"type": "root_add", "group_id": GROUP, "path": path}) +def _ask(s, path: str) -> dict: + """A signed op whose subject is whatever the caller sends.""" + s._dispatch_message({"type": "chat_directory", "path": path}) return s._channel.sent[-1] def test_a_member_cannot_pile_up_challenges(): s = _member_session() for i in range(MAX_PENDING_ADMIN_OPS): - assert _root_add(s, f"/srv/{i}")["type"] == "admin_challenge" - refused = _root_add(s, "/srv/one-too-many") + assert _ask(s, f"/srv/{i}")["type"] == "admin_challenge" + refused = _ask(s, "/srv/one-too-many") assert refused["type"] == "error" and refused["code"] == "too_many_pending" assert len(s._admin_ops) == MAX_PENDING_ADMIN_OPS def test_an_oversized_request_is_not_kept(): s = _member_session() - refused = _root_add(s, "x" * (MAX_ADMIN_OP_BYTES + 1)) + refused = _ask(s, "x" * (MAX_ADMIN_OP_BYTES + 1)) assert refused["type"] == "error" and refused["code"] == "too_large" assert s._admin_ops == {} @@ -72,21 +74,21 @@ def test_an_oversized_request_is_not_kept(): def test_an_expired_challenge_frees_its_place(): s = _member_session() for i in range(MAX_PENDING_ADMIN_OPS): - _root_add(s, f"/srv/{i}") + _ask(s, f"/srv/{i}") for pending in s._admin_ops.values(): pending["ts"] -= 10_000 - assert _root_add(s, "/srv/after-expiry")["type"] == "admin_challenge" + assert _ask(s, "/srv/after-expiry")["type"] == "admin_challenge" assert len(s._admin_ops) == 1 def test_answering_a_challenge_frees_its_place(): s = _member_session() for i in range(MAX_PENDING_ADMIN_OPS): - _root_add(s, f"/srv/{i}") + _ask(s, f"/srv/{i}") op_id = next(iter(s._admin_ops)) s._dispatch_message({"type": "admin_response", "op_id": op_id, "signature": "!!"}) assert len(s._admin_ops) == MAX_PENDING_ADMIN_OPS - 1 - assert _root_add(s, "/srv/next")["type"] == "admin_challenge" + assert _ask(s, "/srv/next")["type"] == "admin_challenge" assert all(time.time() - p["ts"] < 5 for p in s._admin_ops.values()) @@ -95,27 +97,6 @@ def test_answering_a_challenge_frees_its_place(): # The signature covers the subject and nothing else of a request, so every value # the executor acts on has to be in it. -def test_root_add_signs_whether_members_may_write(): - from meshbay_common.adminop import root_add_subject - s = _member_session() - s._dispatch_message({"type": "root_add", "group_id": GROUP, "path": "/srv/drop", - "name": "Drop", "writable": True, "removable": False}) - challenge = s._channel.sent[-1] - assert challenge["subject"] == root_add_subject("/srv/drop", "Drop", "generic", - True, False) - assert challenge["subject"] != root_add_subject("/srv/drop", "Drop", "generic", - False, False) - - -def test_group_attach_signs_the_directory_it_exposes(): - from meshbay_common.adminop import group_attach_subject - s = _member_session() - s._dispatch_message({"type": "group_attach", "name": "photos", - "shared_dir": "/home/me/Photos"}) - assert s._channel.sent[-1]["subject"] == group_attach_subject( - "photos", "/home/me/Photos", True) - - def test_invite_create_signs_the_name_it_records(): from meshbay_common.adminop import invite_create_subject s = _member_session() diff --git a/packages/meshbay-node/tests/test_node_status.py b/packages/meshbay-node/tests/test_node_status.py index bd63f8e..257c60c 100644 --- a/packages/meshbay-node/tests/test_node_status.py +++ b/packages/meshbay-node/tests/test_node_status.py @@ -1,9 +1,10 @@ """ -node_status, root_add, root_remove over MNP. +node_status and root_remove over MNP. These test the D5 node management panel's server-side behaviour: the admin identity check on node_status, the list_groups operation, and the root -add/remove flows through the MNP handlers. +removal flow through the MNP handlers. Adding a root is not an MNP message +(MNP 6.0); `ops.add_root` is still tested here, as the loopback and CLI use it. """ import base64 @@ -478,32 +479,6 @@ async def test_remove_root_succeeds_with_two_roots(tmp_path): assert cfg.roots[0].name == "dir1" -# ── root_add MNP handler ─────────────────────────────────────────────────── - -async def test_root_add_issues_challenge(tmp_path, roster): - session = await _session(tmp_path, roster, operator=True, - node_user_id="grenet") - session._do_root_add({"group_id": GROUP, "path": "/tmp/test"}) - msg = _last(session) - assert msg["type"] == "admin_challenge" - - -async def test_root_add_refuses_without_authority(tmp_path, roster): - session = await _session(tmp_path, roster, operator=False) - session._do_root_add({"group_id": GROUP, "path": "/tmp/test"}) - msg = _last(session) - assert msg["type"] == "error" - assert "authorized" in msg["detail"].lower() - - -async def test_root_add_refuses_missing_fields(tmp_path, roster): - session = await _session(tmp_path, roster, operator=True, - node_user_id="grenet") - session._do_root_add({"group_id": GROUP}) - assert _last(session)["type"] == "error" - assert "Missing" in _last(session)["detail"] - - # ── root_remove MNP handler ──────────────────────────────────────────────── async def test_root_remove_issues_challenge(tmp_path, roster): @@ -677,33 +652,6 @@ async def test_denylist_clear_refused_for_non_admin(tmp_path, roster): assert msg["type"] == "error" -# ── group_attach MNP handler ──────────────────────────────────────────── - -async def test_group_attach_issues_challenge(tmp_path, roster): - session = await _session(tmp_path, roster, operator=True, - node_user_id="grenet") - session._do_group_attach({"name": "test-group", "shared_dir": "/tmp/share"}) - msg = _last(session) - assert msg["type"] == "admin_challenge" - - -async def test_group_attach_refused_without_authority(tmp_path, roster): - session = await _session(tmp_path, roster, operator=False) - session._do_group_attach({"name": "test-group", "shared_dir": "/tmp/share"}) - msg = _last(session) - assert msg["type"] == "error" - assert "authorized" in msg["detail"].lower() - - -async def test_group_attach_refuses_missing_fields(tmp_path, roster): - session = await _session(tmp_path, roster, operator=True, - node_user_id="grenet") - session._do_group_attach({"name": "test-group"}) - msg = _last(session) - assert msg["type"] == "error" - assert "Missing" in msg["detail"] - - # ── node_reload MNP handler ───────────────────────────────────────────── async def test_node_reload_runs_for_admin(tmp_path, roster): diff --git a/packages/meshbay-node/tests/test_root_writable_policy.py b/packages/meshbay-node/tests/test_root_writable_policy.py index 0cd9af8..0dc5d6d 100644 --- a/packages/meshbay-node/tests/test_root_writable_policy.py +++ b/packages/meshbay-node/tests/test_root_writable_policy.py @@ -12,8 +12,9 @@ The properties this holds: A member with an old tab open, or one speaking MNP directly, gets the same answer. That half is pinned in `test_security_regressions.py`, next to the overwrite properties it belongs with; -* the setting is changed by a **signed** operator instruction, or it is a - suggestion any member can undo; +* the setting is changed **on the node's own machine** — the desktop + application over loopback, or the CLI — and never over MNP, where a signature + proves only that the operator's key signed (`test_sharing_is_local_only.py`); * it is stored on the **node**, never the hub. A hub that could decide who writes to the operator's disk would have authority over the node. @@ -26,7 +27,7 @@ from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey -from meshbay_common.adminop import OP_ROOT_EJECT, OP_ROOT_PLUG, OP_ROOT_UPDATE +from meshbay_common.adminop import OP_ROOT_EJECT, OP_ROOT_PLUG from meshbay_common.crypto import generate_gek from meshbay_common.protocol import MNP from meshbay_node.indexer.group_index import GroupIndex @@ -163,34 +164,6 @@ def _capture_challenges(session) -> list[tuple[str, str]]: return issued -async def test_changing_a_roots_flags_needs_a_signature(tmp_path): - """The flags are not applied by the request — only by the signed response.""" - session = _session(tmp_path, "the-operator", operator="the-operator") - issued = _capture_challenges(session) - - session._do_root_update({"group_id": "g" * 32, "root_name": "shared", - "writable": False}) - - assert [op for op, _ in issued] == [OP_ROOT_UPDATE] - assert session._ctx["roots"].roots[0].writable is True, ( - "applied before it was signed") - - -async def test_the_subject_names_the_outcome_not_the_operation(tmp_path): - """ - The operator is shown the subject before signing, so it has to say what will - be true afterwards. "shared" alone would have them authorize a change they - cannot see the direction of. - """ - session = _session(tmp_path, "op", operator="op") - issued = _capture_challenges(session) - - session._do_root_update({"group_id": "g" * 32, "root_name": "shared", - "writable": True, "removable": True}) - - assert issued == [(OP_ROOT_UPDATE, "shared:rw=on,rem=on")] - - async def test_eject_and_plug_are_signed_too(tmp_path): """ Hiding a group's whole library from every member is not a lesser act than @@ -214,8 +187,7 @@ async def test_a_request_with_nobody_to_authorize_it_is_refused(tmp_path): issued = _capture_challenges(session) session._has_admin_authority = lambda: False - session._do_root_update({"group_id": "g" * 32, "root_name": "shared", - "writable": True}) + session._do_root_eject({"group_id": "g" * 32, "root_name": "shared"}) assert issued == [] assert [m for m in session.sent if m.get("type") == "error"] 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..cd550e4 --- /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 +(behind a native dialog in 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 |