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:
@@ -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'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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') })
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user