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

GroupeClés
Administrationadmin.access
Utilisateursusers.view, users.manage
Rôlesroles.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 service

Les 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émentLangueForme
keyanglais<domaine>.<action>, en minuscules
name, description, group_namefrançaisaffiché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.

Sur cette page