[DTP-Worker] Sprint 2 · Générateur plan User Permission (RBAC row-level scope_donnees → Frappe)
Complète le pipeline RBAC (Role + Custom DocPerm déjà livrés) par la dimension row-level. Mapping natif ERPNext v15 des 4 scope_donnees : - entite → User Permission allow=Company (28 templates, user=sentinelle) - own → if_owner (déjà posé par fixtures_gen) - groupe → aucune restriction (vue consolidée) - equipe → pas de dimension native → signalé VPS (jamais mappé, #6) Module userperm_gen/ : permlib/{frappe,builder}, CLI build/validate (refuse d'écrire si invariant KO), userperm.schema.json (validateur maison, zéro pip), 12 tests unittest, README. Job CI rbac-userperm-tests ajouté au gate. Docs SPEC §7 + fixtures_gen README cousues. Régression 72 tests verts, gate OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"""Modèle Frappe/ERPNext v15 : mapping `scope_donnees` RBAC → `User Permission`.
|
||||
|
||||
Pourquoi ce module en plus de `../fixtures_gen`
|
||||
------------------------------------------------
|
||||
Les `Custom DocPerm` (générés par `fixtures_gen`) répondent à la question
|
||||
« QUELS verbes sur QUEL DocType » — mais PAS « SUR QUELLES LIGNES ». La portée
|
||||
row-level du contrat RBAC (`scope_donnees ∈ {own, equipe, entite, groupe}`) est
|
||||
enforcée côté Frappe par deux mécanismes NATIFS distincts :
|
||||
|
||||
- `own` → flag `if_owner` du DocPerm (déjà posé par `fixtures_gen`, ici on
|
||||
ne fait que le constater — aucune `User Permission` requise).
|
||||
- `entite` → un enregistrement `User Permission` (allow=Company) par
|
||||
utilisateur, restreignant l'accès aux docs de SA Company.
|
||||
- `groupe` → AUCUNE restriction (vue consolidée transversale) → pas de
|
||||
`User Permission`.
|
||||
- `equipe` → PAS de dimension row-level native unique dans ERPNext v15
|
||||
(ni « Team » standard). On refuse d'inventer un mécanisme
|
||||
(contrainte #6) : ces rôles sont signalés « à confirmer VPS ».
|
||||
|
||||
Point CLÉ (anti-invention #6) : une `User Permission` Frappe est attachée à un
|
||||
UTILISATEUR, pas à un Rôle. Ce worker ne connaît AUCUN utilisateur réel → il ne
|
||||
peut produire qu'un TEMPLATE par rôle (champ `user` = sentinelle), que l'agent
|
||||
ERPNext matérialisera par utilisateur assigné, côté VPS (jamais ici).
|
||||
|
||||
Références Frappe :
|
||||
- DocType `User Permission` : restreint un utilisateur aux documents dont un
|
||||
champ Link (`allow`) vaut `for_value`. `apply_to_all_doctypes=1` applique la
|
||||
restriction à TOUS les DocTypes liés à `allow` (comportement natif v15).
|
||||
- `Company` : chaque entité juridique OTO = une Company ERPNext (CLAUDE.md
|
||||
§Entités). C'est le Link de restriction pour la portée `entite`.
|
||||
|
||||
Ce module ne touche JAMAIS le VPS : il produit des dicts sérialisables.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Sentinelle du champ `user` d'un template : ce worker n'invente aucun
|
||||
# utilisateur (anti-invention #6). L'agent ERPNext remplace par le vrai e-mail
|
||||
# de chaque utilisateur assigné au rôle, côté VPS.
|
||||
# --------------------------------------------------------------------------- #
|
||||
USER_PLACEHOLDER = "__ASSIGN_PER_USER__"
|
||||
|
||||
# Le DocType Link cible de la restriction `entite` : la Company Frappe native.
|
||||
ALLOW_COMPANY = "Company"
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Mécanismes d'enforcement row-level, un par valeur de scope_donnees.
|
||||
# `mechanism` documente COMMENT la portée est appliquée côté Frappe (traçabilité
|
||||
# pour l'agent ERPNext + le SPEC). Un seul émet un template `User Permission`.
|
||||
# --------------------------------------------------------------------------- #
|
||||
SCOPE_MECHANISM: dict[str, str] = {
|
||||
"own": "docperm_if_owner", # posé par fixtures_gen (if_owner=1)
|
||||
"entite": "user_permission_company",
|
||||
"groupe": "none_consolidated", # vue consolidée → aucune restriction
|
||||
"equipe": "vps_confirm_team", # pas de dimension native → à confirmer
|
||||
}
|
||||
|
||||
# Le seul mécanisme qui produit un enregistrement `User Permission` template.
|
||||
MECH_WITH_TEMPLATE = "user_permission_company"
|
||||
|
||||
# Entités (CLAUDE.md §Entités) qui NE sont PAS une Company restreignable :
|
||||
# « Groupe » = périmètre consolidé transversal (jamais une seule Company).
|
||||
NON_COMPANY_ENTITES: frozenset[str] = frozenset({"Groupe"})
|
||||
|
||||
|
||||
def mechanism_for(scope: str) -> str:
|
||||
"""Mécanisme Frappe natif d'enforcement pour un `scope_donnees` donné."""
|
||||
if scope not in SCOPE_MECHANISM:
|
||||
raise ValueError(
|
||||
f"scope_donnees inconnu (hors contrat RBAC) : {scope!r}. "
|
||||
f"Attendu ∈ {sorted(SCOPE_MECHANISM)}."
|
||||
)
|
||||
return SCOPE_MECHANISM[scope]
|
||||
|
||||
|
||||
def user_permission_template(entite: str) -> dict[str, Any]:
|
||||
"""Template `User Permission` (allow=Company) pour un rôle de portée `entite`.
|
||||
|
||||
`user` est une SENTINELLE : l'agent ERPNext clone ce template une fois par
|
||||
utilisateur assigné au rôle et remplace `user` par son e-mail (côté VPS).
|
||||
`for_value` = l'entité juridique du contrat (à confirmer comme Company VPS).
|
||||
`name` est volontairement omis → Frappe l'auto-nomme à l'import.
|
||||
"""
|
||||
if entite in NON_COMPANY_ENTITES:
|
||||
raise ValueError(
|
||||
f"Portée `entite` incohérente : {entite!r} est un périmètre consolidé "
|
||||
f"(pas une Company). Une portée consolidée doit être `groupe`."
|
||||
)
|
||||
return {
|
||||
"doctype": "User Permission",
|
||||
"user": USER_PLACEHOLDER,
|
||||
"allow": ALLOW_COMPANY,
|
||||
"for_value": entite,
|
||||
"apply_to_all_doctypes": 1,
|
||||
"is_default": 0,
|
||||
"hide_descendants": 0,
|
||||
}
|
||||
Reference in New Issue
Block a user