mayan.apps.cabinetsL'application Cabinets fournit un système de classement hiérarchique de documents sous forme d'arborescence de dossiers virtuels (similaire à un système de fichiers). Un cabinet peut contenir des sous-cabinets (profondeur illimitée) et des documents. Un document peut appartenir à plusieurs cabinets simultanément.
L'arborescence est implémentée avec la bibliothèque django-mptt (Modified Preorder Tree Traversal), ce qui rend la lecture de l'arbre très efficace.
Cabinet (MPTTModel)| Champ | Type | Description |
|---|---|---|
label |
CharField(128) |
Nom du cabinet — unique par niveau (unique_together: parent + label) |
parent |
TreeForeignKey(self, null=True) |
Cabinet parent — null = cabinet racine |
documents |
ManyToManyField(Document) |
Documents contenus dans ce cabinet |
Les champs MPTT gérés automatiquement : lft, rght, tree_id, level.
Représentation textuelle : __str__ retourne get_full_path(), ex. : Comptabilité / Factures / 2024.
| Modèle | Usage |
|---|---|
CabinetSearchResult |
Alias pour les résultats de recherche (colonnes séparées) |
DocumentCabinet |
Alias pour la vue des cabinets d'un document |
Seul le cabinet racine (sans parent) peut avoir une ACL objet.
Les sous-cabinets héritent automatiquement des droits de leur racine.
Conséquence : déplacer un sous-cabinet sous une racine différente modifie immédiatement l'ensemble du contrôle d'accès qui lui est appliqué, ainsi qu'à tous ses descendants.
CabinetBusinessLogicMixin)| Méthode | Description |
|---|---|
get_full_path() |
Retourne le chemin complet depuis la racine, ex. RH / Contrats / 2024 |
get_documents(permission, user) |
Documents du cabinet filtrés par ACL |
get_document_count(user) |
Nombre de documents visibles dans ce cabinet |
get_descendants_document_count(user) |
Nombre de documents dans tout le sous-arbre, filtrés par ACL |
document_add(document, user) |
Ajoute un document + émet l'événement |
document_remove(document, user) |
Retire un document + émet l'événement |
Namespace : cabinets
| Permission | Nom interne | Description |
|---|---|---|
permission_cabinet_view |
cabinet_view |
Voir un cabinet et son contenu |
permission_cabinet_create |
cabinet_create |
Créer un cabinet racine ou un sous-cabinet |
permission_cabinet_delete |
cabinet_delete |
Supprimer un cabinet |
permission_cabinet_edit |
cabinet_edit |
Modifier le label d'un cabinet |
permission_cabinet_add_document |
cabinet_add_document |
Ajouter un document dans un cabinet |
permission_cabinet_remove_document |
cabinet_remove_document |
Retirer un document d'un cabinet |
/cabinets/)| Route | Vue | Permission | Description |
|---|---|---|---|
GET /cabinets/ |
CabinetListView |
cabinet_view |
Liste des cabinets racines |
GET /cabinets/create/ |
CabinetCreateView |
cabinet_create |
Créer un cabinet racine |
GET /cabinets/<id>/ |
CabinetDetailView |
cabinet_view |
Détail avec arbre jsTree + documents |
GET /cabinets/<id>/children/add/ |
CabinetChildAddView |
cabinet_create |
Ajouter un sous-niveau |
GET /cabinets/<id>/delete/ |
CabinetDeleteView |
cabinet_delete |
Supprimer |
GET /cabinets/<id>/edit/ |
CabinetEditView |
cabinet_edit |
Modifier le label |
| Route | Vue | Description |
|---|---|---|
GET /documents/<id>/cabinets/ |
DocumentCabinetListView |
Cabinets d'un document |
POST /documents/<id>/cabinets/add/ |
DocumentCabinetAddView |
Ajouter le document à un cabinet |
POST /documents/<id>/cabinets/remove/ |
DocumentCabinetRemoveView |
Retirer le document d'un cabinet |
POST /documents/multiple/cabinets/add/ |
DocumentCabinetAddView |
Action groupée — ajout |
POST /documents/multiple/cabinets/remove/ |
DocumentCabinetRemoveView |
Action groupée — retrait |
CabinetDetailView hérite de DocumentListView et affiche :
permission_document_viewBase : /api/v4/
| Méthode | Endpoint | Permission | Description |
|---|---|---|---|
GET |
/cabinets/ |
cabinet_view |
Liste tous les cabinets |
POST |
/cabinets/ |
cabinet_create (racine) ou ACL parent (sous-cabinet) |
Créer un cabinet |
GET |
/cabinets/<id>/ |
cabinet_view |
Détails d'un cabinet |
PATCH / PUT |
/cabinets/<id>/ |
cabinet_edit |
Modifier label ou parent ⚠️ |
DELETE |
/cabinets/<id>/ |
cabinet_delete |
Supprimer |
GET |
/cabinets/<id>/documents/ |
cabinet_view + document_view |
Documents du cabinet |
POST |
/cabinets/<id>/documents/add/ |
cabinet_add_document |
Ajouter un document |
POST |
/cabinets/<id>/documents/remove/ |
cabinet_remove_document |
Retirer un document |
GET |
/documents/<id>/cabinets/ |
cabinet_view |
Cabinets d'un document |
CabinetSerializer| Champ | Accès | Description |
|---|---|---|
id |
Lecture | Identifiant DB |
label |
Lecture / Écriture | Nom du cabinet |
parent |
Lecture / Écriture ⚠️ | PK du parent (non filtré par ACL) |
parent_id |
Lecture | ID du parent (lecture seule) |
parent_url |
Lecture | URL API du parent |
full_path |
Lecture | Chemin complet depuis la racine |
children |
Lecture | Sous-cabinets imbriqués (récursif via RecursiveField) |
documents_url |
Lecture | URL de la liste des documents |
url |
Lecture | URL absolue de l'objet |
APICabinetListView.perform_create() applique deux chemins selon la présence du champ parent :
permission_cabinet_create globale (vue)permission_cabinet_create sur le cabinet parent via ACL objetsearch_model_cabinet = SearchModel(
app_label='cabinets',
model_name='CabinetSearchResult',
permission=permission_cabinet_view,
)
| Champ | Label |
|---|---|
id |
ID |
label |
Label |
documents__document_type__label |
Type de document |
documents__label |
Label du document |
documents__description |
Description du document |
documents__uuid |
UUID du document |
documents__files__checksum |
Checksum du fichier |
documents__files__mimetype |
MIME type du fichier |
L'app cabinets enrichit aussi la recherche de documents :
| Champ | Label |
|---|---|
cabinets__id |
Cabinet ID |
cabinets__label |
Cabinets |
Namespace : cabinets
| Événement | Nom interne | Déclencheur |
|---|---|---|
event_cabinet_created |
cabinet_created |
Création d'un cabinet |
event_cabinet_edited |
cabinet_edited |
Modification du label |
event_cabinet_deleted |
cabinet_deleted |
Suppression |
event_cabinet_document_added |
add_document |
Document ajouté |
event_cabinet_document_removed |
remove_document |
Document retiré |
Deux WorkflowAction sont disponibles pour automatiser le classement lors des transitions d'état :
CabinetAddAction — « Add to cabinets »Ajoute le document en cours de workflow dans un ou plusieurs cabinets sélectionnés.
permission_cabinet_add_documentselect2CabinetRemoveAction — « Remove from cabinets »Retire le document d'un ou plusieurs cabinets.
permission_cabinet_remove_documentCabinetAddAction, seule la méthode execute() diffèreExemple d'usage : à la transition « Approuvé → Archivé », ajouter le document au cabinet Archives / 2024 / Validés et le retirer de En cours.
wizard_steps.py)Un WizardStep permet d'ajouter des cabinets au moment de l'upload d'un document via l'assistant de téléversement (sources). L'utilisateur peut sélectionner les cabinets cibles directement pendant l'import.
TreeForeignKey + on_delete=CASCADE).cabinet_deleted n'est émis que si le cabinet supprimé n'est pas une racine (les racines utilisent _event_ignore = True).cabinets/
├── models.py # Cabinet (MPTTModel), CabinetSearchResult, DocumentCabinet
├── model_mixins.py # CabinetBusinessLogicMixin
├── serializers.py # CabinetSerializer ⚠️, CabinetDocumentAddSerializer
├── api_views.py # 6 vues REST
├── views/
│ ├── cabinet_views.py # CabinetListView, DetailView, CreateView, EditView…
│ └── document_views.py # DocumentCabinetAddView, RemoveView, ListView
├── permissions.py # 6 permissions, namespace cabinets
├── events.py # 5 événements
├── search.py # SearchModel cabinet + enrichissement search document
├── workflow_actions.py # CabinetAddAction, CabinetRemoveAction
├── wizard_steps.py # Étape upload — sélection de cabinet
├── widgets.py # jstree_data() — génération JSON pour jsTree
├── urls.py # Routes UI + API
├── apps.py # Enregistrement permissions, colonnes, menus
├── handlers.py # Gestionnaires de signaux
└── migrations/ # 7 migrations (0001→0007)
parent (serializers.py:40-42)Sévérité : HIGH — Confiance : 9/10 — Statut : OUVERT
Le champ parent du CabinetSerializer est déclaré ainsi :
parent = serializers.PrimaryKeyRelatedField(
allow_null=True,
label=_('Parent'),
queryset=Cabinet.objects.all(), # ← TOUS les cabinets, sans ACL
required=False
)
Aucun filtrage ACL n'est appliqué sur ce queryset. Tout utilisateur ayant permission_cabinet_edit peut envoyer :
PATCH /api/v4/cabinets/<id>/
Content-Type: application/json
{"parent": <id_cabinet_restreint>}
pour déplacer un cabinet sous n'importe quel autre cabinet du système, y compris des cabinets auxquels il n'a pas accès.
Comme l'ACL est portée par la racine, ce déplacement modifie immédiatement les droits effectifs du cabinet déplacé et de tous ses descendants. Un attaquant peut :
Remplacer PrimaryKeyRelatedField par FilteredPrimaryKeyRelatedField :
from mayan.apps.rest_api.relations import FilteredPrimaryKeyRelatedField
from .permissions import permission_cabinet_edit
parent = FilteredPrimaryKeyRelatedField(
allow_null=True,
required=False,
source_queryset=Cabinet.objects.all(),
source_permission=permission_cabinet_edit,
)