summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-15 00:50:05 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-15 00:50:05 +0200
commitf4d04741379e77d2ef86cccc93856e590bfe1082 (patch)
treee3a094f9a8778cfcb8b3bd49ca422a28e2434104
parent76724252d08162d4df39090af19796054bf4add8 (diff)
downloadmeshbay-f4d04741379e77d2ef86cccc93856e590bfe1082.tar.gz
feat(logs): keep the username on records the account no longer answers for
The connection log took the name from a join on `users`, and deletion tombstones that row — so every record belonging to a deleted account reported `deleted-3f9a1c`, which is the one answer that helps nobody. The log is kept for a legal retention period precisely so it can say who did what; losing the name at deletion kept the data and lost the point of it. `ip_logs.username` is written as the account is erased, and stays NULL while the account is alive, where the join is better because it cannot go stale. The admin view prefers the stored name when there is one: the join still answers after deletion, just with the tombstone. Releasing the username for re-registration and keeping it in the log are separate things, and the guide now says so. On the node side, the pre-proof audit line records the username the session already knew, instead of leaving the column empty. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-rw-r--r--docs/USERGUIDE.md5
-rw-r--r--docs/meshbay-draft-v5.md3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/admin.py5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/users.py7
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/c5e93b1a2f60_keep_username_on_ip_logs.py31
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/models.py5
-rw-r--r--packages/meshbay-hub/tests/test_account_deletion.py37
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py1
8 files changed, 90 insertions, 4 deletions
diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md
index a8991b1..214b6e9 100644
--- a/docs/USERGUIDE.md
+++ b/docs/USERGUIDE.md
@@ -176,7 +176,10 @@ What deletion does **not** do:
ask the operator — `meshbay-node member unpin <user>` and deleting your files are
their commands to run (§4).
- **It does not erase the connection log.** IP records are kept for their legal retention
- period and stay attributable; that is what they exist for.
+ period and stay attributable: the username is copied onto those rows as the account is
+ deleted, so the log still says *who*, and does not answer `deleted-3f9a1c` for exactly
+ the records anyone would be asking about. Releasing the name for re-registration and
+ keeping it in the log are separate things.
Deletion is refused while you still own a group. Hand the group over or delete it first —
otherwise its members would be stranded. The error names the groups blocking you.
diff --git a/docs/meshbay-draft-v5.md b/docs/meshbay-draft-v5.md
index 68f6843..a8b7e4b 100644
--- a/docs/meshbay-draft-v5.md
+++ b/docs/meshbay-draft-v5.md
@@ -358,7 +358,8 @@ password hash cleared, node linking key dropped, memberships, notifications and
tokens removed, active access tokens refused at once by status check rather than left to
expire. Two things survive on purpose. The IP log is kept for its legal retention period
and stays attributable, since detaching it would keep the data and lose the only thing it
-is for. And **nothing on a node is touched**: files, the pinned identity and the keypair
+is for — the name is copied onto those rows as the account goes, since the join that used
+to supply it would answer with the tombstone. And **nothing on a node is touched**: files, the pinned identity and the keypair
bundle live on machines the hub does not command, which is the same sovereignty that makes
§5.5 work. Deleting the hub account is not an erasure request to the operators who host
you — the operator interface (§5.3) is where that happens. Deletion is refused outright
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/admin.py b/packages/meshbay-hub/src/meshbay_hub/api/admin.py
index efebb75..785731d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/admin.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/admin.py
@@ -315,7 +315,10 @@ async def admin_list_logs(
{
"id": lg.id,
"user_id": lg.user_id,
- "username": uname or "",
+ # The kept name wins: it is only ever written when an account is
+ # deleted, and the join still answers then — with the tombstone,
+ # `deleted-3f9a1c`, which is the one answer that helps nobody.
+ "username": lg.username or uname or "",
"event": lg.event,
"ip_address": lg.ip_address,
"detail": lg.detail,
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/users.py b/packages/meshbay-hub/src/meshbay_hub/api/users.py
index b7b402f..0bfbcac 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/users.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/users.py
@@ -7,7 +7,7 @@ from datetime import datetime, timezone, timedelta
from fastapi import APIRouter, Depends, HTTPException, Request, status
from pydantic import BaseModel, field_validator
-from sqlalchemy import delete, select
+from sqlalchemy import delete, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from meshbay_hub.auth import (
@@ -365,6 +365,11 @@ async def erase_account(db: AsyncSession, user: User) -> dict:
await db.execute(delete(Node).where(Node.user_id == user.id))
username = user.username
+ # Before the name is released: the connection log is kept for its legal
+ # retention period and has to stay readable, which means saying who this was
+ # and not "deleted-3f9a1c". Nothing else keeps it.
+ await db.execute(
+ update(IPLog).where(IPLog.user_id == user.id).values(username=username))
user.username = f"deleted-{user.id[:8]}"
user.email = ""
user.pw_hash = b""
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/c5e93b1a2f60_keep_username_on_ip_logs.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/c5e93b1a2f60_keep_username_on_ip_logs.py
new file mode 100644
index 0000000..cfa3229
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/c5e93b1a2f60_keep_username_on_ip_logs.py
@@ -0,0 +1,31 @@
+"""keep_username_on_ip_logs
+
+The connection log is kept for its legal retention period and is meant to stay
+attributable. It took the name from a join on `users`, and account deletion
+tombstones that row — so the log answered `deleted-3f9a1c` for precisely the
+records someone would be asking about. The name is copied onto the log rows at
+deletion; it stays NULL while the account is alive, where the join is better
+because it cannot go stale.
+
+Revision ID: c5e93b1a2f60
+Revises: b4d82e1c77a9
+Create Date: 2026-08-15
+
+"""
+from typing import Sequence, Union
+
+import sqlalchemy as sa
+from alembic import op
+
+revision: str = 'c5e93b1a2f60'
+down_revision: Union[str, Sequence[str], None] = 'b4d82e1c77a9'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ op.add_column('ip_logs', sa.Column('username', sa.String(64), nullable=True))
+
+
+def downgrade() -> None:
+ op.drop_column('ip_logs', 'username')
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/models.py b/packages/meshbay-hub/src/meshbay_hub/db/models.py
index f749204..2e36989 100644
--- a/packages/meshbay-hub/src/meshbay_hub/db/models.py
+++ b/packages/meshbay-hub/src/meshbay_hub/db/models.py
@@ -252,6 +252,11 @@ class IPLog(Base):
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[str | None] = mapped_column(ForeignKey("users.id")) # null for failed logins
+ # The name this account had, written when it is deleted. The username is
+ # released on deletion and the row itself is tombstoned, so the join that
+ # normally supplies the name would answer "deleted-3f9a1c" for exactly the
+ # records the log exists to answer questions about.
+ username: Mapped[str | None] = mapped_column(String(64))
event: Mapped[str] = mapped_column(String(32), nullable=False)
ip_address: Mapped[str] = mapped_column(String(45), nullable=False) # IPv4 or IPv6
detail: Mapped[str | None] = mapped_column(String(256)) # e.g. username on fail
diff --git a/packages/meshbay-hub/tests/test_account_deletion.py b/packages/meshbay-hub/tests/test_account_deletion.py
index 0ae70f4..cddb5d0 100644
--- a/packages/meshbay-hub/tests/test_account_deletion.py
+++ b/packages/meshbay-hub/tests/test_account_deletion.py
@@ -180,3 +180,40 @@ async def test_only_an_admin_may_delete_someone_else(client, db_session):
me = await client.get("/v1/users/me",
headers={"Authorization": f"Bearer {victim_token}"})
assert me.status_code == 200
+
+
+@pytest.mark.asyncio
+async def test_the_log_still_says_who_it_was(client, db_session):
+ """
+ The point of keeping the log is being able to answer who did what. Taking
+ the name from a join meant the answer became "deleted-3f9a1c" the moment
+ anyone deleted their account — for exactly the records that get asked about.
+ """
+ from meshbay_hub.db.models import IPLog
+
+ token, password = await _register(client, "traceable")
+ uid = (await db_session.execute(
+ select(User.id).where(User.username == "traceable"))).scalar_one()
+
+ await client.request("DELETE", "/v1/users/me",
+ headers={"Authorization": f"Bearer {token}"},
+ json={"auth_key": _auth_key(password, "traceable")})
+
+ rows = (await db_session.execute(
+ select(IPLog).where(IPLog.user_id == uid))).scalars().all()
+ assert rows, "registration should have been logged"
+ assert all(r.username == "traceable" for r in rows), \
+ "the log lost the name it exists to record"
+
+ admin_token, _ = await _register(client, "logreader")
+ from meshbay_hub.db.models import User as U
+ admin = (await db_session.execute(
+ select(U).where(U.username == "logreader"))).scalar_one()
+ admin.role = "admin"
+ await db_session.commit()
+
+ r = await client.get(f"/v1/admin/logs?user_id={uid}",
+ headers={"Authorization": f"Bearer {admin_token}"})
+ assert r.status_code == 200, r.text
+ names = {e["username"] for e in r.json()["logs"]}
+ assert names == {"traceable"}, f"admin view shows {names}"
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
index d654894..572f6fd 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
@@ -964,6 +964,7 @@ class WebRTCPeerSession:
user_id=getattr(self, "_pending_sub", "unknown"),
event="pre_proof_fetch",
ip=self._remote_ip,
+ username=self._username or getattr(self, "_pending_username", ""),
group_id=getattr(self, "_pending_group", "") or "",
detail=mtype,
))