Permissions et rôles
Déclarer des permissions, les grouper, les attribuer à un rôle et filtrer l'interface et les routes.
Le contrôle d'accès repose sur des permissions groupées en rôles. Un utilisateur reçoit des rôles, jamais des permissions directement.
Les permissions du core
| Groupe | Clés |
|---|---|
| Administration | admin.access |
| Utilisateurs | users.view, users.manage |
| Rôles | roles.view, roles.manage |
admin.access conditionne l'entrée dans l'admin. C'est elle que vérifient protectAdmin() et auth: 'admin'.
Déclarer des permissions
Depuis un module
Les déclarer dans le manifest. Le core les ajoute au catalogue avant le seed et les attribue au rôle admin au prochain lrvo-db init.
export const manifest = {
name: '@lrvo/module-blog',
version: '1.0.0',
permissions: [
{ key: 'blog.view', name: 'Voir les billets', description: 'Consultation', group_name: 'Blog' },
{ key: 'blog.manage', name: 'Gérer les billets', description: 'Créer, modifier, supprimer', group_name: 'Blog' },
],
};Depuis le projet
import { registerPermissions } from '@lrvo/core/users';
registerPermissions([
{ key: 'orders.view', name: 'Voir les commandes', description: 'Consultation', group_name: 'Commerce' },
{ key: 'orders.manage', name: 'Gérer les commandes', description: 'CRUD', group_name: 'Commerce' },
]);Dans les deux cas, lancer ensuite :
npx lrvo-db init # installation neuve
npx lrvo-db migrate # projet déjà en serviceLes nouvelles permissions apparaissent alors dans /admin/roles, groupées par group_name, et sont attribuées au rôle admin.
migrate suffit sur un projet existant, et c'est ce qui compte : une permission ajoutée par une version ultérieure du core, ou par un module installé après coup, n'a aucun autre chemin vers une base déjà initialisée. Le seed complet ne tourne qu'à l'init, que rien n'enchaîne. migrate, lui, est enchaîné par lrvo-update et fait partie de toute procédure de déploiement. La resynchronisation y est idempotente : elle n'ajoute et ne met à jour que le catalogue, ne retire jamais une permission, et ne touche à aucun rôle autre que admin.
Conventions de nommage
| Élément | Langue | Forme |
|---|---|---|
key | anglais | <domaine>.<action>, en minuscules |
name, description, group_name | français | affichés dans l'admin |
Deux actions suffisent dans la plupart des cas : view pour la consultation, manage pour tout ce qui modifie. Découper plus finement produit des rôles que personne ne compose correctement.
Préfixer par le domaine du module, pas par le nom du package : blog.view, pas lrvo-module-blog.view.
Vérifier une permission
Sur une route API
Déclarativement, jamais dans le handler.
{ path: '/blog/posts', method: 'POST', handler: handleCreate, auth: 'admin', permission: 'blog.manage' }Le routeur retourne un 403 avant d'appeler le handler.
Dans un composant serveur
import { hasPermission } from '@lrvo/core/users';
const canManage = await hasPermission(session.user.id, 'blog.manage');Dans l'interface
Les entrées de navigation et les widgets portent un champ permission et sont masqués automatiquement.
registerNavItem({ id: 'blog', label: 'Billets', href: '/admin/blog', permission: 'blog.view' });
registerWidget({ id: 'blog-stats', Component: BlogWidget, permission: 'blog.view' });Si tous les items d'une section sont masqués, la section disparaît également.
Masquer n'est pas protéger. Un élément masqué dans l'interface reste accessible par appel direct à l'API. La permission doit être déclarée aux deux endroits : sur la route pour la sécurité, sur l'item de navigation pour l'ergonomie.
Gérer les rôles
Depuis /admin/roles, ou par l'API.
import {
listRoles, createRole, updateRole, deleteRole,
getUserRoles, assignUserRole, revokeUserRole,
} from '@lrvo/core/users';
const role = await createRole({
name: 'Éditeur',
description: 'Rédige et publie les billets',
color: '#3b82f6',
});
await updateRole(role.id, {
name: 'Éditeur',
permissionKeys: ['admin.access', 'blog.view', 'blog.manage'],
});
await assignUserRole(userId, role.id);Un rôle destiné à l'admin doit inclure admin.access, sans quoi ses détenteurs sont redirigés à l'entrée.
Rôles système
Les rôles marqués is_system peuvent être renommés, mais leurs permissions ne sont pas modifiables et ils ne sont pas supprimables. C'est ce qui garantit qu'un rôle admin amputé de admin.access ne peut pas verrouiller toute l'installation.
Garde-fous
Le premier compte reçoit le rôle admin. Il se crée par le formulaire de première configuration, affiché sur /auth tant que la base ne contient aucun compte. C'est le seul moyen d'amorcer une installation neuve.
Un utilisateur ne peut pas se retirer son dernier rôle accordant users.manage. L'endpoint DELETE /lrvo/api/users/:id/roles/:roleId refuse l'opération. Sans cette règle, un administrateur pourrait se retrouver dans l'impossibilité de se redonner la permission. La vérification vit dans le handler API, pas dans revokeUserRole, qui reste une opération de bas niveau.
Ordre d'affichage
CORE_PERMISSION_GROUP_ORDER définit l'ordre canonique des sections du core dans l'interface : Administration, Utilisateurs, Rôles. Le handler /lrvo/api/permissions réserve ces sections en tête, puis ajoute les groupes contribués par les features et les modules dans leur ordre d'enregistrement.
L'ordre des appels à registerPermissions() n'a donc aucun effet sur les sections du core.
Utiliser les constantes côté client
import { PERMISSIONS, PERMISSION_DEFINITIONS, getPermissionGroups } from '@lrvo/core/users/constants';C'est la seule sous-entrée de users importable depuis un fichier 'use client'. Le barrel @lrvo/core/users est serveur : l'importer d'un composant client ferait remonter pg dans le bundle navigateur.