Found by QA (browser fetch against a live app, reproduced): PUT /api/permissions/roles/{id} with body {} returns 200 and deletes every permission assigned to the role.
Root cause: RolePermissionsUpdate.permissions uses default_factory=list (modules/permissions/permissions/contracts/schemas.py), so a body that omits the field validates as an explicit empty list, and PermissionService.set_role_permissions treats the list as the full desired state — deleting all rows not in it.
Impact: an admin (or any client with permissions.manage) sending a malformed body can silently strip a role — including locking the admin role out of the role editor itself. Suggested fix: make permissions required in the update schema (explicit [] stays legal as a deliberate clear-all), so a missing field 422s instead of wiping.
Pre-existing on main (not related to PR #267); found during its QA cycle.
https://claude.ai/code/session_01HMniW4BEhpumUTFnWZVKVc
Found by QA (browser fetch against a live app, reproduced):
PUT /api/permissions/roles/{id}with body{}returns 200 and deletes every permission assigned to the role.Root cause:
RolePermissionsUpdate.permissionsusesdefault_factory=list(modules/permissions/permissions/contracts/schemas.py), so a body that omits the field validates as an explicit empty list, andPermissionService.set_role_permissionstreats the list as the full desired state — deleting all rows not in it.Impact: an admin (or any client with permissions.manage) sending a malformed body can silently strip a role — including locking the admin role out of the role editor itself. Suggested fix: make
permissionsrequired in the update schema (explicit[]stays legal as a deliberate clear-all), so a missing field 422s instead of wiping.Pre-existing on main (not related to PR #267); found during its QA cycle.
https://claude.ai/code/session_01HMniW4BEhpumUTFnWZVKVc