summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node')
-rw-r--r--packages/meshbay-node/src/meshbay_node/ui/app.py36
-rw-r--r--packages/meshbay-node/tests/test_roster_pairing.py27
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)