70e022ccd6
- roleprofile_gen/ : profilelib (frappe Role Profile + Has Role natif v15, builder déterministe), roleprofile.schema.json, CLI build/validate refusant d'écrire si invariant cassé, 11 tests stdlib, README, .gitignore (out/). - 6 profils couvrant les 50 rôles de façon bijective (5 portails métier + console technique plateforme). Aucun DocType custom (que du natif). - Anti-invention #6 : rôles 100 % issus du contrat, nom de profil = convention déterministe dérivée de la clé portail. - CI : job rbac-roleprofile-tests ajouté au gate (.gitea/workflows/ci.yml). - Doc : SPEC §7 ét.5 + encart livré. Daily report session 8. - Régression : 83 tests verts (72 + 11). Gate CI local vert. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
81 lines
3.1 KiB
Python
81 lines
3.1 KiB
Python
"""Construction des `Role Profile` 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 « bundle » déterministe :
|
|
- role_profile → 1 fixture `Role Profile` par portail distinct du contrat,
|
|
chacun bundlant les `erpnext_role_name` de ses rôles.
|
|
- manifest → traçabilité (comptes, portails métier vs technique, mapping
|
|
portail → profil), 100 % dérivé du contrat (contrainte #6).
|
|
|
|
Le contrat porte 6 portails distincts (5 métier listés dans `portails_business`
|
|
+ la console technique `plateforme`). Chaque rôle a EXACTEMENT un `portail` →
|
|
la couverture des 50 rôles par les profils est bijective (invariant vérifié par
|
|
le CLI). La source de vérité des comptes est le JSON, pas la doc (SPEC §3).
|
|
|
|
Déterminisme : profils triés par clé de portail, rôles triés par nom Frappe →
|
|
sortie reproductible bit-à-bit à contrat constant (diffable, re-générable en CI
|
|
sans horodatage volatil).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from . import frappe
|
|
|
|
|
|
def _roles_by_portail(contract: dict) -> dict[str, list[str]]:
|
|
"""Regroupe les `erpnext_role_name` par `portail` (ordre trié, stable)."""
|
|
grouped: dict[str, list[str]] = {}
|
|
for r in contract["roles"]:
|
|
portail = r["portail"]
|
|
name = r["erpnext_role_name"]
|
|
grouped.setdefault(portail, []).append(name)
|
|
for portail in grouped:
|
|
names = grouped[portail]
|
|
if len(names) != len(set(names)):
|
|
raise ValueError(f"Rôle dupliqué dans le portail {portail!r}.")
|
|
names.sort()
|
|
return grouped
|
|
|
|
|
|
def build_bundle(contract: dict) -> dict[str, Any]:
|
|
"""Transforme le contrat RBAC en bundle de `Role Profile` déterministe."""
|
|
grouped = _roles_by_portail(contract)
|
|
portails_metier = set(contract.get("portails_business", []))
|
|
|
|
role_profiles: list[dict] = []
|
|
profiles_meta: list[dict] = []
|
|
for portail in sorted(grouped):
|
|
role_names = grouped[portail]
|
|
role_profiles.append(frappe.role_profile_fixture(portail, role_names))
|
|
profiles_meta.append(
|
|
{
|
|
"portail": portail,
|
|
"role_profile": frappe.role_profile_name(portail),
|
|
"metier": portail in portails_metier,
|
|
"nb_roles": len(role_names),
|
|
}
|
|
)
|
|
|
|
nb_metier = sum(1 for p in profiles_meta if p["metier"])
|
|
manifest = {
|
|
"generated_from": "rbac_50_roles.json",
|
|
"source_version": contract["version"],
|
|
"cible_rbac_roles": contract["cible_rbac_roles"],
|
|
"counts": {
|
|
"role_profiles": len(role_profiles),
|
|
"portails_metier": nb_metier,
|
|
"portails_techniques": len(role_profiles) - nb_metier,
|
|
"roles_couverts": sum(p["nb_roles"] for p in profiles_meta),
|
|
},
|
|
# Mapping portail → profil (métier vs console technique `plateforme`),
|
|
# pour que l'agent ERPNext assigne le bon profil à chaque utilisateur.
|
|
"profiles": profiles_meta,
|
|
}
|
|
|
|
return {
|
|
"manifest": manifest,
|
|
"role_profile": role_profiles,
|
|
}
|