permissions est le système de contrôle d'accès global de Mayan. Là où les ACLs accordent l'accès à un objet précis, les permissions accordent l'accès à tous les objets d'un type pour un utilisateur appartenant au bon rôle.
La relation complète est : Utilisateur → Groupe → Rôle → Permission
C'est le point le plus important à comprendre dans cette app.
Mayan a besoin que les permissions soient à la fois :
if permission_document_view ... sans requête SQLLa solution : deux objets distincts qui se synchronisent.
Permission — la permission volatile (en mémoire)Existe uniquement dans le code Python. Elle est créée au démarrage de Django via le fichier permissions.py de chaque app, et disparaît à l'arrêt du processus. Elle n'a pas d'id en base.
# mayan/apps/documents/permissions.py
namespace = PermissionNamespace(name='documents', label=_('Documents'))
permission_document_view = namespace.add_permission(
name='document_view',
label=_('View documents')
)
permission_document_edit = namespace.add_permission(
name='document_edit',
label=_('Edit documents')
)
Sa clé primaire est une simple chaîne : "documents.document_view". Elle vit dans Permission._registry — un dictionnaire Python en mémoire.
On l'utilise dans le code pour les vérifications :
Permission.check_user_permission(
permission=permission_document_view,
user=request.user
)
StoredPermission — la permission persistante (en base)Existe uniquement en base de données. C'est un enregistrement avec un id numérique, deux champs texte (namespace et name), et rien d'autre. Elle ne sait pas ce qu'elle représente sans le code.
Table permissions_storedpermission :
┌────┬────────────┬──────────────────┐
│ id │ namespace │ name │
├────┼────────────┼──────────────────┤
│ 1 │ documents │ document_view │
│ 2 │ documents │ document_edit │
│ 3 │ ocr │ ocr_document │
└────┴────────────┴──────────────────┘
Elle est créée automatiquement la première fois qu'une permission volatile est utilisée, via get_or_create. Ce cached_property dans Permission fait le pont :
# classes.py
@cached_property
def stored_permission(self):
stored_permission, created = StoredPermission.objects.get_or_create(
name=self.name, namespace=self.namespace.name
)
return stored_permission
On l'utilise comme FK dans les tables de relation :
# Le rôle "Comptabilité" reçoit la permission de voir les documents
role_comptabilite.permissions.add(permission_document_view.stored_permission)
Code Python (démarrage) Base de données
───────────────────────── ──────────────────────────────────
Permission volatile StoredPermission
pk = "documents.document_view" ←→ namespace="documents", name="document_view"
label = "View documents" id = 1
namespace = PermissionNamespace
↑
Permission._registry["documents.document_view"]
(dictionnaire en mémoire, rechargé à chaque démarrage)
Role.permissions (M2M) → StoredPermission
ACL.permissions (M2M) → StoredPermission
RoleUn rôle est la seule entité à laquelle on peut accorder des permissions. Il relie :
StoredPermission M2M)Group M2M)Utilisateur ──► Groupe ──► Rôle ──► StoredPermission (globale)
└──► ACL sur objet X → StoredPermission (par objet)
Un utilisateur hérite des permissions de tous les rôles de tous ses groupes.
Cas spéciaux :
is_superuser ou is_staff → accès total, aucune vérificationScénario : l'utilisatrice Marie doit pouvoir voir tous les documents.
1. Déclaration dans le code (permissions.py)
permission_document_view = namespace.add_permission(
name='document_view', label=_('View documents')
)
2. En base, StoredPermission est créée automatiquement au premier accès
permissions_storedpermission : id=1, namespace="documents", name="document_view"
3. Dans l'interface admin : créer un rôle et lui accorder la permission
role = Role.objects.create(label="Lecteurs")
role.permissions.add(permission_document_view.stored_permission)
4. Ajouter Marie au groupe lié au rôle
group_lecteurs.roles.add(role) # via Role.groups M2M
marie.groups.add(group_lecteurs)
5. Vérification dans une vue
# Soit globalement (lève PermissionDenied si refusé)
Permission.check_user_permission(
permission=permission_document_view, user=marie
)
# Soit pour filtrer un queryset
queryset = AccessControlList.objects.restrict_queryset(
permission=permission_document_view,
queryset=Document.objects.all(),
user=marie
)
# → Marie a la permission globale, donc tout le queryset est retourné
Si une permission est supprimée du code (renommée, app désactivée) mais existe encore en base, elle devient orpheline. Son label affiche "Unknown or obsolete permission: document_view". Pour nettoyer :
./manage.py permissions_purge_obsolete --settings=mayan.settings.development
| Fichier | Rôle |
|---|---|
classes.py |
Permission, PermissionNamespace — le registre en mémoire |
models.py |
Role, StoredPermission — la persistance en base |
model_mixins.py |
Logique métier : user_has_this(), grant(), revoke(), groups_add() |
managers.py |
StoredPermissionManager.purge_obsolete() |
permissions.py |
(dans chaque app) Déclaration des namespaces et permissions |
aclsCes deux apps sont complémentaires et forment ensemble le système de sécurité de Mayan :
permissions |
acls |
|
|---|---|---|
| Portée | Globale (tous les objets du type) | Par objet spécifique |
| Accordé à | Rôle | Rôle + objet |
| Exemple | "Peut voir tous les documents" | "Peut voir ce cabinet uniquement" |
| Vérification | Permission.check_user_permission() |
AccessControlList.objects.check_access() |