fix(auth): prevent admin from revoking their last users.manage role
- add self-lockout guard in handleRevokeUserRole api handler - sequence role additions before removals and handle delete errors in UserEditModal - document the security rule in core/users README
This commit is contained in:
@@ -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.
|
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
|
### Permissions
|
||||||
|
|||||||
@@ -123,22 +123,33 @@ const UserEditModal = ({ userId, currentUserId, isOpen, onClose, onSaved }) => {
|
|||||||
const toAdd = selectedRoleIds.filter(id => !initialRoleIds.includes(id));
|
const toAdd = selectedRoleIds.filter(id => !initialRoleIds.includes(id));
|
||||||
const toRemove = initialRoleIds.filter(id => !selectedRoleIds.includes(id));
|
const toRemove = initialRoleIds.filter(id => !selectedRoleIds.includes(id));
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all(
|
||||||
...toAdd.map(roleId =>
|
toAdd.map(roleId =>
|
||||||
fetch(`/zen/api/users/${userId}/roles`, {
|
fetch(`/zen/api/users/${userId}/roles`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: JSON.stringify({ roleId }),
|
body: JSON.stringify({ roleId }),
|
||||||
})
|
})
|
||||||
),
|
)
|
||||||
...toRemove.map(roleId =>
|
);
|
||||||
|
|
||||||
|
const removeResults = await Promise.all(
|
||||||
|
toRemove.map(roleId =>
|
||||||
fetch(`/zen/api/users/${userId}/roles/${roleId}`, {
|
fetch(`/zen/api/users/${userId}/roles/${roleId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
credentials: 'include',
|
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 (emailChanged) {
|
||||||
if (isSelf) {
|
if (isSelf) {
|
||||||
|
|||||||
@@ -526,8 +526,26 @@ async function handleAssignUserRole(request, { id: userId }) {
|
|||||||
// DELETE /zen/api/users/:id/roles/:roleId (admin only)
|
// 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 {
|
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);
|
await revokeUserRole(userId, roleId);
|
||||||
return apiSuccess({ success: true });
|
return apiSuccess({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user