diff --git a/src/core/users/README.md b/src/core/users/README.md index f7efd85..106284c 100644 --- a/src/core/users/README.md +++ b/src/core/users/README.md @@ -216,6 +216,8 @@ await revokeUserRole(userId, roleId); Les rôles système (`is_system = true`) peuvent être renommés mais leurs permissions ne peuvent pas être modifiées. Ils ne peuvent pas être supprimés. +L'endpoint `DELETE /zen/api/users/:id/roles/:roleId` applique une règle de sécurité supplémentaire : un utilisateur ne peut pas se retirer un rôle qui lui accorde `users.manage` s'il n'en a pas d'autre. Cela évite qu'un administrateur se retrouve dans l'impossibilité de se redonner la permission. Cette vérification est faite au niveau du handler API et ne concerne pas la fonction `revokeUserRole` elle-même. + --- ### Permissions diff --git a/src/features/admin/components/UserEditModal.client.js b/src/features/admin/components/UserEditModal.client.js index 05530be..dc982bd 100644 --- a/src/features/admin/components/UserEditModal.client.js +++ b/src/features/admin/components/UserEditModal.client.js @@ -123,22 +123,33 @@ const UserEditModal = ({ userId, currentUserId, isOpen, onClose, onSaved }) => { const toAdd = selectedRoleIds.filter(id => !initialRoleIds.includes(id)); const toRemove = initialRoleIds.filter(id => !selectedRoleIds.includes(id)); - await Promise.all([ - ...toAdd.map(roleId => + await Promise.all( + toAdd.map(roleId => fetch(`/zen/api/users/${userId}/roles`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ roleId }), }) - ), - ...toRemove.map(roleId => + ) + ); + + const removeResults = await Promise.all( + toRemove.map(roleId => fetch(`/zen/api/users/${userId}/roles/${roleId}`, { method: 'DELETE', credentials: 'include', - }) - ), - ]); + }).then(async res => ({ res, data: await res.json() })) + ) + ); + + const failedRemove = removeResults.find(({ res }) => !res.ok); + if (failedRemove) { + toast.error(failedRemove.data?.message || failedRemove.data?.error || 'Impossible de retirer ce rôle'); + onSaved?.(); + onClose(); + return; + } if (emailChanged) { if (isSelf) { diff --git a/src/features/auth/api.js b/src/features/auth/api.js index 43f934d..799036b 100644 --- a/src/features/auth/api.js +++ b/src/features/auth/api.js @@ -526,8 +526,26 @@ async function handleAssignUserRole(request, { id: userId }) { // DELETE /zen/api/users/:id/roles/:roleId (admin only) // --------------------------------------------------------------------------- -async function handleRevokeUserRole(_request, { id: userId, roleId }) { +async function handleRevokeUserRole(_request, { id: userId, roleId }, context) { try { + if (context.session.user.id === userId) { + const roleHasPerm = await query( + `SELECT 1 FROM zen_auth_role_permissions WHERE role_id = $1 AND permission_key = $2`, + [roleId, PERMISSIONS.USERS_MANAGE] + ); + if (roleHasPerm.rows.length > 0) { + const otherRoles = await query( + `SELECT 1 FROM zen_auth_user_roles ur + JOIN zen_auth_role_permissions rp ON rp.role_id = ur.role_id + WHERE ur.user_id = $1 AND rp.permission_key = $2 AND ur.role_id != $3 + LIMIT 1`, + [userId, PERMISSIONS.USERS_MANAGE, roleId] + ); + if (otherRoles.rows.length === 0) { + return apiError('Forbidden', "Vous ne pouvez pas retirer ce rôle car c'est votre seule source de la permission de gestion des utilisateurs."); + } + } + } await revokeUserRole(userId, roleId); return apiSuccess({ success: true }); } catch (error) {