b1b011ab81
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>
83 lines
3.3 KiB
Python
83 lines
3.3 KiB
Python
"""Construction du plan `User Permission` depuis `rbac_50_roles.json`.
|
|
|
|
Entrée : le contrat RBAC 50 rôles (déjà validé par `rbac.schema.json` dans le
|
|
job CI `rbac-tests`). Sortie : un « plan » déterministe qui, pour CHAQUE rôle,
|
|
indique le mécanisme Frappe d'enforcement row-level de son `scope_donnees` et,
|
|
le cas échéant, un template `User Permission` (allow=Company) à matérialiser par
|
|
utilisateur côté VPS.
|
|
|
|
Déterminisme : tri stable des entrées par nom de rôle Frappe → sortie
|
|
reproductible bit-à-bit à contrat constant (diffable, re-générable en CI).
|
|
|
|
Anti-invention (#6) : 100 % des données proviennent du contrat (nom du rôle,
|
|
scope, entité). Aucun utilisateur n'est inventé (sentinelle `USER_PLACEHOLDER`).
|
|
Les rôles `equipe` (pas de dimension row-level native) ne reçoivent AUCUN
|
|
template et sont signalés « à confirmer VPS » plutôt que mappés arbitrairement.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from . import frappe
|
|
|
|
|
|
def _sorted_roles(contract: dict) -> list[dict]:
|
|
return sorted(contract["roles"], key=lambda r: r["erpnext_role_name"])
|
|
|
|
|
|
def build_plan(contract: dict) -> dict[str, Any]:
|
|
"""Transforme le contrat RBAC en plan `User Permission` déterministe."""
|
|
roles = _sorted_roles(contract)
|
|
|
|
plan: list[dict] = []
|
|
companies: set[str] = set() # for_value distincts (portée entite)
|
|
equipe_roles: list[str] = [] # rôles equipe → à confirmer VPS
|
|
by_mechanism: dict[str, int] = {m: 0 for m in frappe.SCOPE_MECHANISM.values()}
|
|
|
|
for r in roles:
|
|
role_name = r["erpnext_role_name"]
|
|
scope = r["scope_donnees"]
|
|
entite = r["entite_principale"]
|
|
mechanism = frappe.mechanism_for(scope)
|
|
by_mechanism[mechanism] += 1
|
|
|
|
template = None
|
|
if mechanism == frappe.MECH_WITH_TEMPLATE:
|
|
# Lève ValueError si le contrat couple `entite` à un périmètre
|
|
# consolidé (« Groupe ») — garde-fou anti-contrat corrompu.
|
|
template = frappe.user_permission_template(entite)
|
|
companies.add(entite)
|
|
elif scope == "equipe":
|
|
equipe_roles.append(role_name)
|
|
|
|
plan.append(
|
|
{
|
|
"erpnext_role_name": role_name,
|
|
"scope_donnees": scope,
|
|
"entite_principale": entite,
|
|
"mechanism": mechanism,
|
|
"user_permission_template": template,
|
|
}
|
|
)
|
|
|
|
manifest = {
|
|
"generated_from": "rbac_50_roles.json",
|
|
"source_version": contract["version"],
|
|
"cible_rbac_roles": contract["cible_rbac_roles"],
|
|
"counts": {
|
|
"plan_entries": len(plan),
|
|
"user_permission_templates": by_mechanism[frappe.MECH_WITH_TEMPLATE],
|
|
"by_mechanism": by_mechanism,
|
|
},
|
|
# Companies ERPNext à CRÉER/confirmer sur le VPS avant d'appliquer les
|
|
# User Permission (les noms viennent de entite_principale du contrat).
|
|
"companies_a_confirmer": sorted(companies),
|
|
# Rôles `equipe` : aucune dimension row-level native → l'agent ERPNext
|
|
# doit décider du mécanisme (champ custom + User Permission, ou
|
|
# hiérarchie Employee.reports_to). Jamais mappé en douce ici (#6).
|
|
"roles_scope_equipe_a_confirmer": sorted(equipe_roles),
|
|
}
|
|
|
|
return {"manifest": manifest, "plan": plan}
|