From 77e9c2535323b99c6205a5dd5cc9c7f987dfa03c Mon Sep 17 00:00:00 2001 From: Lukas Remis Date: Sun, 25 Oct 2026 15:39:33 +0100 Subject: [PATCH] Admin: allow changing your own quota The self-protection guard refused every change to the admin's own account. It now only refuses blocking and deleting yourself; a quota change is fine. Co-Authored-By: Claude Opus 5.5 --- backend/app/api/admin.py | 17 +++++++++++------ backend/tests/api/test_admin_users.py | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index b2b2e72..45fc0ca 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -3,8 +3,9 @@ The backend seam for the admin panel: the registration gate — the first account on a fresh install becomes the administrator and closes signup behind itself, and this is how that gets reopened — the default quota, and managing users -(search, per-user quota, block, delete). An administrator cannot block or delete -their own account, so the instance can never lock out its last admin that way. +(search, per-user quota, block, delete). An administrator can change their own +quota but cannot block or delete their own account, so the instance can never +lock out its last admin that way. """ from __future__ import annotations @@ -280,12 +281,15 @@ def _user_out(user: User, used_bytes: int, accounts: list[str]) -> AdminUserOut: ) -async def _target(db: AsyncSession, admin: User, user_id: uuid.UUID) -> User: - """The user an admin action is aimed at; never the admin themselves.""" +async def _target( + db: AsyncSession, admin: User, user_id: uuid.UUID, *, allow_self: bool = False +) -> User: + """The user an admin action is aimed at. Blocking and deleting are never + allowed on the admin themselves; a quota change is (``allow_self``).""" user = await db.scalar(select(User).where(User.id == user_id)) if user is None: raise_api_error(404, "not_found", "user not found") - if user.id == admin.id: + if user.id == admin.id and not allow_self: raise_api_error(409, "cannot_modify_self", "you cannot do this to your own account") return user @@ -303,7 +307,8 @@ async def patch_user( user_id: uuid.UUID, body: AdminUserPatch, admin: CurrentAdmin, db: DbSession ) -> AdminUserOut: """Change a user's quota and/or block or unblock them.""" - user = await _target(db, admin, user_id) + # Your own quota is fine to change; blocking yourself is not. + user = await _target(db, admin, user_id, allow_self=body.blocked is None) if body.quota_bytes is not None: user.quota_bytes = body.quota_bytes if body.blocked is not None: diff --git a/backend/tests/api/test_admin_users.py b/backend/tests/api/test_admin_users.py index dcfafa4..2a8d8f1 100644 --- a/backend/tests/api/test_admin_users.py +++ b/backend/tests/api/test_admin_users.py @@ -168,6 +168,20 @@ async def test_admin_cannot_block_or_delete_themselves(world: World) -> None: assert (await c.delete(f"/api/admin/users/{admin.id}")).status_code == 409 +async def test_admin_can_change_their_own_quota(world: World) -> None: + admin = await world.user(admin=True) + async with await world.client(admin) as c: + resp = await c.patch(f"/api/admin/users/{admin.id}", json={"quota_bytes": 5 * 2**30}) + assert resp.status_code == 200 + assert resp.json()["quota_bytes"] == 5 * 2**30 + # Quota together with a block is still refused: the block is the problem. + assert ( + await c.patch( + f"/api/admin/users/{admin.id}", json={"quota_bytes": 2**30, "blocked": True} + ) + ).status_code == 409 + + async def test_delete_removes_the_user_and_their_data(world: World, bus: _Bus) -> None: admin = await world.user(admin=True) target = await world.user()