diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-01 14:08:32 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-01 14:08:32 +0200 |
| commit | cfc91e0a424163869c64d30e55d55a53f18a3dbf (patch) | |
| tree | 04ed3bbec11690f62f1e2a738bf89f4b763332e5 /packages/meshbay-node/tests | |
| parent | ba45a3c94806f612fa62812e0b36d08b581a2e47 (diff) | |
| download | meshbay-cfc91e0a424163869c64d30e55d55a53f18a3dbf.tar.gz | |
refactor(node): JSON-only control API, Node page absorbs the admin dashboard
Remove the node daemon's server-rendered admin UI (GET / and /audit, the
_render_* helpers and inline templates) and the `meshbay-node ui` CLI verb.
The loopback control API stays; it is now JSON only, ruff-clean, and 453
lines (was 1074). Also drop three never-wired endpoints (/api/config,
/api/chat/history, /ws/chat, plus broadcast_chat) and the pointless
18000/tcp firewall profiles.
The desktop client's Node page (static/node-page.js) takes over what the
dashboard showed, reorganised into six tabs (Overview, Groups, Roster,
Peers, Audit, Settings):
- Overview: version, node id, QUIC port, hub, index-cache maintenance
- Roster: node-wide view with unpin
- Peers and Audit: auto-load on open, no Load button
- Audit: real usernames and group names (resolved from the roster and
node.toml), Previous/Next pagination newest-first, Export CSV of every
matching row
- Settings: node settings, STUN, ICE, denylist, then Unlink from hub
Backend: audit.get_entries gains `offset`; /api/audit and /api/peers
resolve ids to names via a new _display_names helper; CSP tightened to
default-src 'none' now that no HTML is served. draft-v6 sections 2.11 and
2.12 corrected -- the Node page uses the loopback API, not MNP.
One capability is intentionally dropped: browser-based admin on a headless
server. The CLI covers every operation there.
See docs/refactor-node-ui.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQCaZnde4Bjjdu84dhSuF5
Diffstat (limited to 'packages/meshbay-node/tests')
| -rw-r--r-- | packages/meshbay-node/tests/test_audit.py | 21 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/test_cli_dispatch.py | 6 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/test_security_regressions.py | 62 |
3 files changed, 46 insertions, 43 deletions
diff --git a/packages/meshbay-node/tests/test_audit.py b/packages/meshbay-node/tests/test_audit.py index b2ed15e..8755be2 100644 --- a/packages/meshbay-node/tests/test_audit.py +++ b/packages/meshbay-node/tests/test_audit.py @@ -53,6 +53,27 @@ async def test_filter_by_user(audit): @pytest.mark.asyncio +async def test_pagination_newest_first(audit): + for i in range(5): + await audit.log_event(user_id="u1", event="connect", detail=f"e{i}") + # distinct timestamps so ORDER BY is deterministic + await audit._db.execute( + "UPDATE audit_log SET timestamp = ? WHERE detail = ?", + (1000 + i, f"e{i}")) + await audit._db.commit() + + page1 = await audit.get_entries(limit=2, offset=0) + page2 = await audit.get_entries(limit=2, offset=2) + page3 = await audit.get_entries(limit=2, offset=4) + + assert [e.detail for e in page1] == ["e4", "e3"] # newest first + assert [e.detail for e in page2] == ["e2", "e1"] + assert [e.detail for e in page3] == ["e0"] # last, partial page + # offset past the end is empty, not an error + assert await audit.get_entries(limit=2, offset=99) == [] + + +@pytest.mark.asyncio async def test_entry_count(audit): assert await audit.entry_count() == 0 await audit.log_event(user_id="u1", event="connect") diff --git a/packages/meshbay-node/tests/test_cli_dispatch.py b/packages/meshbay-node/tests/test_cli_dispatch.py index d912b43..b676546 100644 --- a/packages/meshbay-node/tests/test_cli_dispatch.py +++ b/packages/meshbay-node/tests/test_cli_dispatch.py @@ -24,7 +24,6 @@ from meshbay_node import daemon as daemon_mod # would otherwise stop for a confirmation nobody can type in a test. VERBS = [ ["status"], - ["ui"], ["group", "list"], ["group", "add"], # missing --dir: usage, then exit ["gek", "init"], @@ -135,6 +134,11 @@ def test_the_verb_list_here_matches_the_parser(): f"CLI verbs with no dispatch test: {sorted(untested)} — add them to " f"VERBS above") + # The server-rendered admin UI (and its `ui` verb) were removed in + # docs/refactor-node-ui.md phase 5. The control API stays; the browser + # page does not. + assert "ui" not in declared, "the `ui` verb came back" + @pytest.mark.parametrize("argv,verb", [ (["reload"], "reload"), diff --git a/packages/meshbay-node/tests/test_security_regressions.py b/packages/meshbay-node/tests/test_security_regressions.py index 725b800..10182d3 100644 --- a/packages/meshbay-node/tests/test_security_regressions.py +++ b/packages/meshbay-node/tests/test_security_regressions.py @@ -650,56 +650,34 @@ def test_node_admin_ui_requires_token(): assert client.get("/api/status").status_code == 403 assert client.get("/api/status?t=wrong").status_code == 403 - assert client.get("/api/config?t=wrong").status_code == 403 + assert client.get("/api/groups?t=wrong").status_code == 403 assert client.get("/api/status?t=secret-token").status_code == 200 assert client.get( "/api/status", headers={"X-MeshBay-Token": "secret-token"} ).status_code == 200 -def test_admin_ui_escapes_filenames(tmp_path): +def test_node_control_api_serves_no_html(): """ - H2: filenames are chosen by any group member and were rendered into the - localhost admin UI unescaped, giving script execution against an - unauthenticated admin API. - """ - from meshbay_node.ui.app import _render_page - - payload = '<img src=x onerror="fetch(1)">' - index = GroupIndex(group_id="g" * 32, sk_node=Ed25519PrivateKey.generate()) - index.add_entry(IndexEntry( - id="0" * 64, name=payload, path="", size=1, type="video", added_at=0, - )) - - html = _render_page({ - "status": "running", - "groups_ctx": {"g" * 32: {"index": index, "roots": one_root(tmp_path)}}, - "indexes": {"g" * 32: index}, - }) - - assert payload not in html, "filename rendered unescaped — stored XSS (H2)" - assert "<img" in html, "filename should appear escaped" + H2 was stored XSS in the server-rendered admin dashboard: a member-chosen + filename, or a hub-supplied username, landed in an HTML page on the + operator's machine unescaped. That dashboard is gone + (docs/refactor-node-ui.md phase 5) — the control API is JSON only, so there + is no server-side template to inject into. The Node page that replaced it + ships in the desktop client and escapes by default (Preact). - -def test_admin_ui_escapes_roster_usernames(tmp_path): - """ - H2 again, for the roster: usernames originate at the hub and land on the - operator's own admin page, which can re-key groups and read the audit log. + This locks the removal in: the HTML routes stay 404, and the render helpers + stay deleted so nothing reintroduces a template by importing one. """ - from meshbay_node.ui.app import _render_page + import meshbay_node.ui.app as ui_app + from fastapi.testclient import TestClient - payload = '<img src=x onerror="fetch(1)">' - html = _render_page( - {"status": "running", "groups_ctx": {}, "indexes": {}}, - { - "identities": {"u1": {"user_id": "u1", "username": payload, - "pk_ed25519": "AAA", "pinned_at": "now", - "pinned_via": "code"}}, - "members": [{"group_id": "", "user_id": "u1", "role": "operator", - "status": "active"}], - "invites": [], - }, - ) + app = ui_app.create_ui_app({"status": "running", "groups_ctx": {}, + "indexes": {}, "ui_token": "t"}) + client = TestClient(app) + for path in ("/", "/audit", "/dashboard"): + assert client.get(f"{path}?t=t").status_code == 404, path - assert payload not in html, "username rendered unescaped — stored XSS (H2)" - assert "<img" in html + for gone in ("_render_page", "_render_audit_page", "_render_roster", + "_AUDIT_HTML"): + assert not hasattr(ui_app, gone), f"{gone} came back — HTML surface" |