diff options
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/ui/app.py | 36 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/test_roster_pairing.py | 27 |
2 files changed, 61 insertions, 2 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/ui/app.py b/packages/meshbay-node/src/meshbay_node/ui/app.py index f5cd5d0..9ed014b 100644 --- a/packages/meshbay-node/src/meshbay_node/ui/app.py +++ b/packages/meshbay-node/src/meshbay_node/ui/app.py @@ -371,15 +371,47 @@ def create_ui_app(state: dict) -> FastAPI: """Map a username to an account id, through the hub.""" return await _op(lambda: ops.resolve_user(state, username)) + async def _audit_local(event: str, subject: str, group_id: str = "") -> None: + """ + Record a removal made at the machine (Node page or CLI) in audit.db. + + The signed door over WebRTC records its own (`_audit` in the session); + this one has no session. The actor is the node's own account — whoever + holds the control API's token acts as it — and the address is the + loopback, which tells the two doors apart in the log. + """ + audit = state.get("audit_store") + if not audit: + return + try: + await audit.log_event( + user_id=state.get("node_user_id") or "", + event=event, + ip="127.0.0.1", + username=getattr(getattr(state.get("config"), "hub", None), "username", "") or "", + group_id=group_id, + detail=subject, + ) + except Exception as e: + log.warning("audit: could not record %s of %s: %s", event, subject[:8], e) + @app.post("/api/members/{user_id}/revoke") async def revoke_member(user_id: str, group_id: str): """Stop serving the group key to a member.""" - return await _op(lambda: ops.revoke_member(state, user_id, group_id)) + async def run(): + result = await ops.revoke_member(state, user_id, group_id) + await _audit_local("member_revoke", user_id, group_id) + return result + return await _op(run) @app.post("/api/members/{user_id}/unpin") async def unpin_member(user_id: str): """Forget a pinned identity, so the person can pair again with a new key.""" - return await _op(lambda: ops.unpin_member(state, user_id)) + async def run(): + result = await ops.unpin_member(state, user_id) + await _audit_local("member_unpin", user_id) + return result + return await _op(run) # ── Chat encryption (operator only, localhost) ───────────────────────── diff --git a/packages/meshbay-node/tests/test_roster_pairing.py b/packages/meshbay-node/tests/test_roster_pairing.py index b2799e9..e2180ba 100644 --- a/packages/meshbay-node/tests/test_roster_pairing.py +++ b/packages/meshbay-node/tests/test_roster_pairing.py @@ -845,6 +845,33 @@ async def test_unpin_endpoint_allows_repairing(tmp_path, roster): assert client.post("/api/members/bob/unpin?t=tok").status_code == 404 +async def test_a_removal_at_the_machine_is_in_the_audit_log(tmp_path, roster): + """The Node page and the CLI remove through here; the signed WebRTC door + already wrote its own row, this one wrote none.""" + from meshbay_node.audit import AuditStore + + audit = AuditStore(db_path=tmp_path / "audit.db") + await audit.open() + try: + client, _ = _ui_client(tmp_path, roster, audit_store=audit) + _, pk_ed_b64, pk_x_b64 = _keypair() + await roster.pin_identity("bob", "bob", pk_ed_b64, pk_x_b64, "code") + await roster.set_member(GROUP, "bob", ROLE_MEMBER, "active", "grenet") + + assert client.post(f"/api/members/bob/revoke?group_id={GROUP}&t=tok").status_code == 200 + assert client.post("/api/members/bob/unpin?t=tok").status_code == 200 + # A refusal changed nothing, so it records nothing. + assert client.post("/api/members/bob/unpin?t=tok").status_code == 404 + + rows = sorted(await audit.get_entries(), key=lambda e: e.id) + assert [(e.event, e.user_id, e.ip, e.group_id, e.detail) for e in rows] == [ + ("member_revoke", "grenet", "127.0.0.1", GROUP, "bob"), + ("member_unpin", "grenet", "127.0.0.1", "", "bob"), + ] + finally: + await audit.close() + + async def test_operator_surface_needs_the_session_token(tmp_path, roster): """11.5.3 applies to every one of these: they change who may hold the key.""" client, _ = _ui_client(tmp_path, roster) |