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 <noreply@anthropic.com>
This commit is contained in:
@@ -3,8 +3,9 @@
|
|||||||
The backend seam for the admin panel: the registration gate — the first account
|
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,
|
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
|
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
|
(search, per-user quota, block, delete). An administrator can change their own
|
||||||
their own account, so the instance can never lock out its last admin that way.
|
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
|
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:
|
async def _target(
|
||||||
"""The user an admin action is aimed at; never the admin themselves."""
|
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))
|
user = await db.scalar(select(User).where(User.id == user_id))
|
||||||
if user is None:
|
if user is None:
|
||||||
raise_api_error(404, "not_found", "user not found")
|
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")
|
raise_api_error(409, "cannot_modify_self", "you cannot do this to your own account")
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@@ -303,7 +307,8 @@ async def patch_user(
|
|||||||
user_id: uuid.UUID, body: AdminUserPatch, admin: CurrentAdmin, db: DbSession
|
user_id: uuid.UUID, body: AdminUserPatch, admin: CurrentAdmin, db: DbSession
|
||||||
) -> AdminUserOut:
|
) -> AdminUserOut:
|
||||||
"""Change a user's quota and/or block or unblock them."""
|
"""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:
|
if body.quota_bytes is not None:
|
||||||
user.quota_bytes = body.quota_bytes
|
user.quota_bytes = body.quota_bytes
|
||||||
if body.blocked is not None:
|
if body.blocked is not None:
|
||||||
|
|||||||
@@ -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
|
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:
|
async def test_delete_removes_the_user_and_their_data(world: World, bus: _Bus) -> None:
|
||||||
admin = await world.user(admin=True)
|
admin = await world.user(admin=True)
|
||||||
target = await world.user()
|
target = await world.user()
|
||||||
|
|||||||
Reference in New Issue
Block a user