fix(core) : RBAC review fixes - code readonly in edit, TOCTOU doc, canManage reactive, itemsPerPage 999

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-04-16 11:17:13 +02:00
parent d49c317c49
commit 793c58a4a8
5 changed files with 25 additions and 14 deletions

View File

@@ -19,8 +19,7 @@
:label="t('admin.roles.form.code')"
input-class="w-full"
required
:readonly="isEditMode && role?.isSystem"
:hint="isEditMode && role?.isSystem ? t('admin.roles.delete.systemTooltip') : ''"
:readonly="isEditMode"
/>
<MalioInputTextArea
@@ -121,7 +120,7 @@ const permissionsByModule = computed<PermissionModule[]>(() => {
async function loadPermissions() {
const data = await api.get<{ member: Permission[] }>(
'/permissions',
{ 'orphan': false, itemsPerPage: 200 },
{ 'orphan': false, itemsPerPage: 999 },
{ toast: false },
)
allPermissions.value = data.member
@@ -183,19 +182,24 @@ function handleToggleAll(module: string, selected: boolean) {
async function handleSave() {
saving.value = true
try {
const body = {
label: form.value.label,
code: form.value.code,
description: form.value.description || null,
permissions: Array.from(selectedPermissionIds.value).map(id => `/api/permissions/${id}`),
}
const permissions = Array.from(selectedPermissionIds.value).map(id => `/api/permissions/${id}`)
if (isEditMode.value && props.role) {
await api.patch(`/roles/${props.role.id}`, body, {
// Le code est immuable apres creation (garde backend RoleProcessor)
await api.patch(`/roles/${props.role.id}`, {
label: form.value.label,
description: form.value.description || null,
permissions,
}, {
toastSuccessMessage: t('admin.roles.toast.updated'),
})
} else {
await api.post('/roles', body, {
await api.post('/roles', {
label: form.value.label,
code: form.value.code,
description: form.value.description || null,
permissions,
}, {
toastSuccessMessage: t('admin.roles.toast.created'),
})
}

View File

@@ -186,7 +186,7 @@ const effectivePermissions = computed<EffectivePermission[]>(() => {
async function loadData() {
const [rolesData, permsData] = await Promise.all([
api.get<{ member: Role[] }>('/roles', {}, { toast: false }),
api.get<{ member: Permission[] }>('/permissions', { orphan: false, itemsPerPage: 200 }, { toast: false }),
api.get<{ member: Permission[] }>('/permissions', { orphan: false, itemsPerPage: 999 }, { toast: false }),
])
allRoles.value = rolesData.member
allPermissions.value = permsData.member

View File

@@ -81,7 +81,7 @@ import type { Role } from '~/shared/types/rbac'
const { t } = useI18n()
const api = useApi()
const { can } = usePermissions()
const canManage = can('core.roles.manage')
const canManage = computed(() => can('core.roles.manage'))
useHead({ title: t('admin.roles.title') })

View File

@@ -56,7 +56,7 @@ const { can } = usePermissions()
useHead({ title: t('admin.users.title') })
const canManage = can('core.users.manage')
const canManage = computed(() => can('core.users.manage'))
const users = ref<UserListItem[]>([])
const loading = ref(false)

View File

@@ -53,6 +53,13 @@ final class AdminHeadcountGuard implements AdminHeadcountGuardInterface
* La verification est volontairement conservative (<=1) pour couvrir
* le cas defensif ou la base serait deja dans un etat incoherent (0 admin).
*
* TOCTOU accepte : la verification n'utilise pas de verrou pessimiste
* (SELECT ... FOR UPDATE). Deux demotions concurrentes pourraient donc
* passer le garde simultanement. Ce risque est accepte dans le contexte
* PME/CRM ou les operations d'administration sont rares et mono-operateur.
* Si la concurrence admin devient un enjeu, ajouter un verrou pessimiste
* sur countAdmins() ou une contrainte CHECK en base.
*
* @throws LastAdminProtectionException si le nombre d'admins est inferieur ou egal a 1
*/
private function checkAdminHeadcount(): void