diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-11 19:27:27 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-11 19:27:27 +0200 |
| commit | d8885c8df17c60927cb8d1f77ce1745814c6d3b4 (patch) | |
| tree | fafa41edfc7adcb25d6c46d4bbf2d0fba93ed010 /packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py | |
| parent | edbff1768054afa80efda721cd1011b29c7fe355 (diff) | |
| download | meshbay-d8885c8df17c60927cb8d1f77ce1745814c6d3b4.tar.gz | |
fix(hub): an administrator can erase an account that owns groups
An administrator's deletion answered 409 for any account owning a group,
so an erasure ordered by an authority had to wait on the person it was
about. It now deletes the account's groups with it, then pushes a signed
revocation for the account and for each group to every connected node:
an access token already issued stays valid on a node until it expires,
and the revocation is what makes the nodes refuse the account and close
the groups' sessions now. The action is written to the IP log, and the
confirmation dialog says the groups go too, in all ten catalogues.
The owner's own deletion is unchanged: refused while they own groups,
which they can hand over first (CGU 3.4, privacy statement).
Deleting a group had three partial cascades. The owner's route left
email_verifications behind, and the cleanup of unhosted groups left
notifications, invitations and reports - each an IntegrityError on
PostgreSQL, invisible on SQLite, which does not enforce foreign keys by
default. db/purge.py is now the one implementation: it finds every table
referencing groups.id from the schema, deletes the group's rows and
detaches content reports, which are evidence and outlive the group.
test_group_purge.py turns foreign-key enforcement on for its connection,
seeds every referencing table, and fails without the fix on all three
routes. MESHBAY_DESIGN.md 7.7 states the rule, and now lists the device
keys and swarm sources that e3c68b3 erases.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9MCBBWSm9GhBESmqzJxNy
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py | 17 |
1 files changed, 9 insertions, 8 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py b/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py index 6fa62a4..7f8a5f2 100644 --- a/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py +++ b/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py @@ -7,7 +7,7 @@ from datetime import datetime, timedelta, timezone from sqlalchemy import delete, select from sqlalchemy.ext.asyncio import AsyncSession -from meshbay_hub.db.models import EmailVerification, Group, GroupMember, IPLog, User +from meshbay_hub.db.models import EmailVerification, Group, IPLog, User log = logging.getLogger(__name__) @@ -90,18 +90,19 @@ async def prune_unhosted_groups(db: AsyncSession, grace_days: int = UNHOSTED_GRA dry_run: bool = False) -> list[tuple[str, str]]: """Delete abandoned groups. Returns [(id, name)] of what was (or would be) removed. - Memberships go with the group — there is no cascade configured, and leaving - orphan rows behind would keep the group in everyone's /mine query through the - join. Nothing on a node is touched: the hub does not command those machines, - and by definition no node ever claimed this group anyway. + Everything on the hub that points at the group goes with it (`purge_groups`): + there is no cascade configured, an orphan membership would keep the group in + everyone's /mine query through the join, and on PostgreSQL any remaining + reference refuses the deletion outright. Nothing on a node is touched: the + hub does not command those machines, and by definition no node ever claimed + this group anyway. """ doomed = await find_unhosted_groups(db, grace_days) if not doomed or dry_run: return [(g.id, g.name) for g in doomed] - ids = [g.id for g in doomed] - await db.execute(delete(GroupMember).where(GroupMember.group_id.in_(ids))) - await db.execute(delete(Group).where(Group.id.in_(ids))) + from meshbay_hub.db.purge import purge_groups + await purge_groups(db, [g.id for g in doomed]) await db.commit() log.info("Pruned %d group(s) that no node ever hosted", len(doomed)) return [(g.id, g.name) for g in doomed] |