Le contrôle d'accès (RBAC) dans Forge (forge-mvc-rbac)¶
Ce document explique ce que fait l'opt-in forge-mvc-rbac, ce qu'il expose, et comment on s'en sert.
forge-mvc-rbac protège les routes par permissions, organisées en rôles, avec trois gardes selon la source des permissions, un helper Jinja can(), et des commandes rbac:validate / rbac:audit.
Toutes les gardes échouent fermé (401/403) : en cas de doute, l'accès est refusé.
1. Rôle du module
Au-delà de « connecté ou non », une application doit dire « cet utilisateur a-t-il le droit de faire ceci ».
L'opt-in répond à cette question via des permissions (article.update) regroupées en rôles (editor), et des gardes à poser sur les contrôleurs.
Il propose trois niveaux, qui ne sont pas trois façons de faire la même chose mais trois contextes distincts selon l'origine des permissions.
2. Installation
Prérequis : activez le venv du projet
Quelle que soit la source, installez dans le venv du projet :
Lancé hors d'un venv, pip vise le Python système (Debian 12+, Ubuntu 23.04+),
protégé par PEP 668. Il refuse alors d'installer, pour ne pas écraser les paquets
gérés par apt, et affiche externally-managed-environment.
Le venv de projet créé par forge new n'a pas ce verrou.
Installer le paquet¶
B. Depuis Git (avant-garde)¶
Cœur puis opt-in depuis git, dans le venv du projet (l'opt-in trouve le cœur git déjà en place, sans version publiée sur PyPI) :
3. Mise en service
Installer le paquet ne suffit pas à le rendre opérationnel.
Voici les gestes propres à forge-mvc-rbac, dans l'ordre.
Ils déclinent la procédure canonique, Rendre un opt-in opérationnel : les cinq
points.
1. L'épingler¶
Dans requirements.txt, à la même version ou au même commit que forge-mvc.
Sans cette ligne, l'opt-in n'existe que sur votre machine.
2. L'inscrire¶
L'opt-in est inscrit dans optins/registry.py (ADR-061), ce qui le rend visible du
projet.
--apply est obligatoire : sans lui, la commande simule et n'écrit rien.
3. Poser ce dont il a besoin¶
rbac:init copie la migration embarquée dans mvc/migrations/ ;
migration:apply l'exécute et la trace (ADR-071).
Sans cette étape, le premier appel échoue sur une table absente.
4. Le brancher là où il agit¶
Il se branche dans app.py, là où l'application compose ses middlewares et ses
fournisseurs de contexte. Ce câblage vous appartient : Forge ne l'écrit jamais à
votre place (principe 9).
5. Le prouver¶
Puis un premier usage réel.
Un opt-in installé, inscrit et provisionné qu'aucun code n'appelle n'est pas
opérationnel : il est seulement présent.
4. Désinstallation
opt-in:disable est l'inverse d'enable : il dé-inscrit du registre, sans toucher au paquet.
forge opt-in:remove rbac affiche la commande pip uninstall sans l'exécuter.
5. Commandes
forge-mvc-rbac ajoute ces commandes :
| Commande | Rôle | Exemple |
|---|---|---|
rbac:validate |
Valide mvc/security/rbac.json contre le schéma. |
forge rbac:validate |
rbac:audit |
Audit de cohérence fonctionnelle du contrat. | forge rbac:audit |
6. Vue d'ensemble rapide
| Élément | Valeur |
|---|---|
| Paquet | forge-mvc-rbac |
| Module | forge_mvc_rbac |
| Catégorie | Sécurité et accès (ADR-055) |
| Couche | opt-in (brique optionnelle), transversal aux routes |
| Dépend de | forge-mvc |
| Gardes de route | require_contract_permission, require_user_permission, require_permission |
| Contrat | mvc/security/rbac.json, validé par rbac.schema.json (embarqué, ADR-056) |
| Helper Jinja | can() (via make_auth_jinja_can) |
| Commandes | rbac:validate, rbac:audit |
| Comportement | échec fermé (401/403) |
| Décisions d'architecture | ADR-014 (emplacement du contrat), ADR-056 (schéma + outillage) |
| Installation | pip install --pre forge-mvc-rbac |
7. Schémas UML
Les deux schémas suivants montrent deux vues complémentaires de l'opt-in.
Le diagramme de classe montre les trois gardes, le contrat et le helper Jinja.
Le diagramme de séquence montre une route protégée par le contrat.
5.1 Diagramme de classe¶
Le diagramme de classe montre les trois gardes selon la source des permissions, et le contrat RBAC chargé depuis un fichier.
classDiagram
direction LR
class contrat {
<<module>>
+load_rbac_contract(root) RbacContractResult
+require_contract_permission(perm)
+has_contract_permission(...)
+get_request_roles(request)
}
class authorization {
<<module>>
+require_user_permission(perm)
+auth_user_can(user, perm)
}
class rbac {
<<module>>
+require_permission(perm)
+has_permission(request, perm)
+make_can(...)
}
class jinja {
<<module>>
+make_auth_jinja_can()
}
class rbac_json {
<<contrat>>
+roles
+entities/permissions
}
contrat --> rbac_json : charge
jinja --> contrat : expose can()
contrat ..> RbacContractError : peut lever
À retenir :
require_contract_permissionlit le contrat (déclaratif, sans base) ;require_user_permissionrésout depuis l'utilisateur connecté (base) ;require_permissionlit des permissions déjà chargées (bas niveau) ;can()expose la même logique aux templates.
5.2 Diagramme de séquence¶
Le diagramme de séquence montre une route protégée par le contrat RBAC.
sequenceDiagram
actor Utilisateur
participant Route as Contrôleur protégé
participant RBAC as require_contract_permission
participant Contrat as rbac.json (chargé)
Utilisateur->>Route: requête sur une action protégée
Route->>RBAC: vérifie "article.update"
RBAC->>Contrat: rôles de la requête -> permissions
alt permission accordée
RBAC-->>Route: autorisé
Route-->>Utilisateur: réponse
else permission absente
RBAC-->>Utilisateur: 403 (échec fermé)
end
À retenir :
- la garde s'exécute avant l'action du contrôleur ;
- les rôles de la requête sont résolus en permissions ;
- une permission manquante renvoie 403 (jamais un accès par défaut) ;
- le contrat décrit qui peut quoi, hors du code.
8. API publique
Trois gardes de route (selon le contexte)¶
| Garde | Source des permissions | Quand |
|---|---|---|
require_contract_permission |
contrat rbac.json chargé (sans base) |
recommandé, déclaratif, voie officielle |
require_user_permission / auth_user_can |
utilisateur Auth/User connecté (base) | permissions stockées en base |
require_permission / has_permission |
request.permissions (déjà peuplé) |
primitive bas niveau |
Contrat¶
| Élément | Rôle |
|---|---|
load_rbac_contract(root) -> RbacContractResult |
charge et valide mvc/security/rbac.json |
has_contract_permission, get_contract_permissions, get_request_roles |
lecture du contrat |
RbacContractError, RbacContractResult |
erreurs et résultat |
Modèle et Jinja¶
| Élément | Rôle |
|---|---|
Role, Permission, PermissionDenied |
modèle RBAC |
make_can, make_auth_jinja_can, make_auth_jinja_context_with_can |
helper can() pour les templates |
9. Contextes d'utilisation
| Besoin | Élément |
|---|---|
| Protéger une route (défaut) | @require_contract_permission("article.update") |
| Protéger selon l'utilisateur en base | require_user_permission(...) / auth_user_can(...) |
| Garde bas niveau | require_permission(...) (permissions pré-chargées) |
| Afficher un bouton conditionnel | {% if can("article.update") %} |
| Décrire les droits | mvc/security/rbac.json |
| Vérifier le contrat | forge rbac:validate / forge rbac:audit |
10. Exemples d'utilisation
8.1 Protéger une route par le contrat (recommandé)¶
from forge_mvc_rbac import require_contract_permission
def update(request):
refus = require_contract_permission(contract, user_roles, "article.update")
if refus is not None:
return refus
...
Ce n'est pas un décorateur : la fonction rend None si la permission est accordée, et une Response 403 si elle est refusée ou si le contrat est absent.
Le contrôleur la teste et retourne le refus tel quel, ce qui garde le contrôle de flux visible (principe 3).
Les droits sont décrits dans mvc/security/rbac.json, pas codés en dur.
8.2 Conditionner l'affichage dans un template¶
can() est exposé aux templates via make_auth_jinja_can (même logique que les gardes).
Aide-mémoire
Une question, trois sources :
- contrat (
require_contract_permission) : déclaratif, par défaut ; - base (
require_user_permission) : permissions de l'utilisateur connecté ; - bas niveau (
require_permission) : permissions déjà chargées.
11. Contrat, sécurité et validation
Le contrat mvc/security/rbac.json décrit les rôles et les permissions, séparément du schéma d'entité (ADR-014).
Son schéma rbac.schema.json est embarqué par cet opt-in (ADR-056).
forge rbac:validate vérifie la conformité au schéma ; forge rbac:audit repère les incohérences fonctionnelles (permission déclarée mais inutilisée, action CRUD sans permission).
Échec fermé
Toutes les gardes refusent l'accès en cas de doute (401 si non authentifié, 403 si non autorisé).
Une permission absente n'ouvre jamais l'accès « par défaut ».
Une seule façon par défaut
require_contract_permission est la voie officielle (déclarative, sans base), promue par le parcours welcome-rbac.
Les deux autres gardes existent pour des contextes précis (permissions en base, primitive bas niveau), pas comme alternatives interchangeables.
Indépendance du cœur
Le cœur de Forge ne dépend pas de forge-mvc-rbac ; le provider Jinja can() se branche au chargement du paquet (mécanisme de loader, ADR-046).
12. RBAC léger core ou RBAC complet opt-in ?
Forge distingue deux niveaux d'autorisation :
RBAC léger core : primitives dans core/security/ (dépréciées, héritées du développement pré-1.0) :
user_has_role(request, role): vérifie qu'un rôle est présent dans le champrolesde la session Auth/User.
Ne consulte pas les tables SQL RBAC.require_role(role): décorateur qui redirige vers/loginsi non authentifié, retourne 403 si le rôle est absent de la session.
Ces deux fonctions conviennent aux cas les plus simples (protéger une route par un rôle déjà dans la session).
Elles ne connaissent pas les permissions fines et ne remplacent pas forge-mvc-rbac.
Les nouveaux projets utilisent forge_mvc_rbac.require_user_permission.
RBAC complet opt-in : module forge-mvc-rbac :
- Modèles
Role,Permission(normalisation, validation) - Décorateur
@require_permission(...): lit les permissions injectées dans la requête ou la session (RBAC historique, sans accès base) ; la résolution SQL viaroles,permissions,role_permissionsest faite parrequire_user_permission - Helper Jinja
make_can/can(...): affichage conditionnel dans les templates - Résolution backend
get_user_permissions,user_has_permission - Pont Auth/User vers RBAC via la table
user_roles - Administration CLI des associations utilisateurs/rôles
Quand utiliser quoi ?¶
| Besoin | Choix recommandé |
|---|---|
| Vérifier simplement qu'un utilisateur a un rôle (session) | user_has_role (core léger, déprécié) |
| Protéger une route pour les nouveaux projets | forge-mvc-rbac, require_user_permission |
Permissions fines (contacts.edit, posts.delete) |
forge-mvc-rbac, require_user_permission (autoritatif, base) |
| Administrer rôles et permissions | forge-mvc-rbac |
| Affichage conditionnel dans les templates Jinja | forge-mvc-rbac, can(...) |
| Relations utilisateurs/rôles complexes | forge-mvc-rbac |
Frontière d'import¶
core/ ne doit pas importer forge_mvc_rbac.
La dépendance va dans un seul sens : forge-mvc-rbac → core.
core/auth/audit.py peut nommer des événements d'audit RBAC génériques : ce vocabulaire est assumé dans le core (ADR-011), il ne représente pas une dépendance fonctionnelle vers le module opt-in.
13. Modèle contrat autonome (résolveur, garde par préfixe, provider)
Ces trois briques rendent le modèle contrat (rbac.json) autonome sous l'auth moderne, sans les tables du modèle table.
11.1 Résolution des rôles en base (resolver.py)¶
get_user_role_slugs(user_id) fait le pont user_roles -> roles.slug : il fournit les slugs de rôles d'un utilisateur au modèle contrat, ce qui permet à get_request_roles de résoudre les rôles sous l'auth moderne sans injection préalable.
11.2 Garde par préfixe d'URL (prefix_guard.py)¶
PrefixPermissionMiddleware est une garde RBAC par préfixe d'URL (et non route par route).
On lui passe une table préfixe -> permission ; à chaque requête, le préfixe le plus spécifique qui matche impose sa permission contractuelle (403 sinon).
Il couvre des domaines entiers, y compris leurs routes futures, sans décoration ni passe sur le routeur.
Il s'installe comme middleware d'Application et s'adosse au contrat (get_request_roles + has_contract_permission).
11.3 Provider Jinja du modèle contrat¶
Pour un can() de template adossé au contrat (rbac.json), jinja.py expose make_contract_jinja_can, make_contract_jinja_context et register_contract_rbac_provider, en complément du provider table par défaut.
Voir aussi¶
- Cœur RBAC (rbac.py) : modèle, primitives,
make_can. - Contrat RBAC (contract.py) :
load_rbac_contract, gardes contrat. - Autorisation Auth/User (authorization.py) : gardes basées sur la base.
- Résolveur de permissions (resolver.py) et Liens utilisateur/rôle (user_rbac.py).
- Helpers Jinja (jinja.py) :
can(). - Contrat RBAC séparé et RBAC, usage applicatif.
- Welcome-RBAC : parcours d'apprentissage.