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

View File

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

View File

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

View File

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

View File

@@ -53,6 +53,13 @@ final class AdminHeadcountGuard implements AdminHeadcountGuardInterface
* La verification est volontairement conservative (<=1) pour couvrir * La verification est volontairement conservative (<=1) pour couvrir
* le cas defensif ou la base serait deja dans un etat incoherent (0 admin). * 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 * @throws LastAdminProtectionException si le nombre d'admins est inferieur ou egal a 1
*/ */
private function checkAdminHeadcount(): void private function checkAdminHeadcount(): void