[DTP-Worker] Sprint 8 · buffer · RBAC/userperm_gen : la TABLE « Mapping scope_donnees → mécanisme » — la FONCTION d'enforcement ROW-LEVEL (quel mécanisme Frappe natif applique CHAQUE portée + si un User Permission template est émis), CŒUR sécurité du module — était transcrite EN PROSE (README:33-38) SANS AUCUN gate d'IDENTITÉ. Le bloc RBAC « 3 volets » ne gate QUE la ventilation par mécanisme (« 28/16/2/4 »), aveugle à QUEL mécanisme applique QUELLE portée.
Gate ajouté (check_readme_claims.sh) : par portée le mechanism (1er token backtické) + le verdict « Template émis ? » recomputés de out/user_permission_plan.json byte-gaté, exacts ; identité d'ensemble portées table == portées artefact (ni fantôme ni manquante) ; cohérences croisées : chaque portée mappe UN SEUL mécanisme (fonction) · verdict uniforme par portée · template émis SSI user_permission_company (invariant « template SSI entite »). 7 morsures vérifiées (README réaffecte entite→none_consolidated = row-level enforcement abandonné · bascule verdict oui→non · échange portée = fantôme+manquant · retrait ligne · table supprimée · artefact réaffecte entite · artefact non-uniforme casse SSI) ; restauré = green · exit 0. Working tree byte-restauré (git checkout --, JAMAIS git clean) · 7 gates re-verts. ci/README.md (récap + détail 2e surface userperm_gen) mis à jour.
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
# Activity Log · 2026-08-01 · Claude Code DTP
|
||||||
|
|
||||||
|
## Session `20260801_000201` · Buffer S8 · Domaine RBAC/userperm_gen : la **table « Mapping `scope_donnees` → mécanisme »** — la **FONCTION d'enforcement ROW-LEVEL** (quel mécanisme Frappe natif applique CHAQUE portée de données + si un `User Permission` template est émis), CŒUR sécurité du module — était transcrite EN PROSE (README:33-38) SANS AUCUN gate d'IDENTITÉ. Le bloc RBAC « 3 volets » existant ne gate QUE la **ventilation** par mécanisme (« 28 `entite` · 16 `groupe` · 2 `own` · 4 `equipe` »), un COMPTE aveugle à QUEL mécanisme applique QUELLE portée.
|
||||||
|
|
||||||
|
**Tâche** : **Sprint 8 · buffer** (DevOps CI/CD · QA). Roadmap fonctionnellement
|
||||||
|
close ; poursuite de la série anti-dérive (CLAUDE.md #6). Même patron d'IDENTITÉ que
|
||||||
|
le mapping RBAC `fixtures_gen` (séparation des pouvoirs `set_user_permissions`) ou la
|
||||||
|
cross-cohérence PERMISSIONS Legal/CONFOTUR (`role_id`→actions) — appliqué à la
|
||||||
|
**surface data-derived distincte du même README `rbac/userperm_gen`** : la table
|
||||||
|
qui associe chaque portée de données à son mécanisme d'enforcement, jamais gatée hors
|
||||||
|
son compte agrégé.
|
||||||
|
|
||||||
|
**Dérive silencieuse fermée** :
|
||||||
|
- `05_deliverables_mvp/rbac/userperm_gen/README.md:33-38` — table « Mapping
|
||||||
|
`scope_donnees` → mécanisme » : nomme, PAR portée, le `mechanism` Frappe natif
|
||||||
|
(`own`→`docperm_if_owner` · `entite`→`user_permission_company` ·
|
||||||
|
`groupe`→`none_consolidated` · `equipe`→`vps_confirm_team`) ET le verdict
|
||||||
|
« Template émis ? » (`oui` pour `entite` seul, sinon `non (null)`).
|
||||||
|
- Source faisant autorité (byte-gatée par `check_artifacts`) :
|
||||||
|
`out/user_permission_plan.json` — chaque entrée porte `scope_donnees` +
|
||||||
|
`mechanism` + `user_permission_template` (null ou objet). Le mapping est une
|
||||||
|
**fonction** : chaque portée → exactement un mécanisme ; template émis SSI
|
||||||
|
`entite`/`user_permission_company`.
|
||||||
|
- Piège : le bloc de ventilation est AVEUGLE à l'identité de ces couples.
|
||||||
|
RÉAFFECTER une portée à un mécanisme plus permissif (`entite` →
|
||||||
|
`none_consolidated` : le **row-level enforcement ABANDONNÉ**, sur-exposition des
|
||||||
|
données inter-entités — la restriction même que le module pose), RENOMMER un
|
||||||
|
mécanisme ou BASCULER le verdict « Template émis ? » laisse le README périmé
|
||||||
|
pendant que l'artefact dit autre chose → l'agent ERPNext Backend câblerait le
|
||||||
|
mauvais mécanisme (le risque même que la table veut prévenir). Aucune suite
|
||||||
|
`tests/` (qui teste des FONCTIONS de mapping/résolution, pas la prose) n'attrape
|
||||||
|
ce « vert trompeur ».
|
||||||
|
- **État courant** : **aucune ligne périmée** — les 4 couples portée→mécanisme et
|
||||||
|
leurs verdicts recoupent l'artefact exactement (anti-invention #6, rien à
|
||||||
|
réécrire). Le défaut est la **surface ungated**.
|
||||||
|
|
||||||
|
**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « RBAC/userperm_gen
|
||||||
|
Mapping » après le bloc CRM gardes) : (1) **par portée** — le `mechanism` (1er token
|
||||||
|
backtické de la colonne) ET le verdict « Template émis ? » (`**oui**` / `non (null)`)
|
||||||
|
recomputés de `user_permission_plan.json`, prose exigée EXACTE ; (2) **identité
|
||||||
|
d'ensemble** — portées de la table == portées de l'artefact (set-diff : ni fantôme
|
||||||
|
ni manquante). Cohérences croisées en bonus (mordent un plan INTERNEMENT incohérent) :
|
||||||
|
chaque portée mappe UN SEUL mécanisme (fonction, pas relation) · « Template émis »
|
||||||
|
UNIFORME sur les entrées d'une portée · template émis EXACTEMENT pour
|
||||||
|
`user_permission_company` (l'invariant « template SSI `entite` » du README:36/61) ·
|
||||||
|
ensemble des portées NON VIDE. Un claim absent échoue AUSSI (traçabilité).
|
||||||
|
|
||||||
|
**7 morsures vérifiées** : README réaffecte `entite`→`none_consolidated`
|
||||||
|
(mécanisme périmé · élévation de portée) · README bascule `entite` « Template
|
||||||
|
émis » oui→non (verdict périmé) · README échange une portée `groupe`→`famille`
|
||||||
|
(absents=[groupe] + en trop=[famille] + ligne INTROUVABLE) · README retire la
|
||||||
|
ligne `own` (absents=[own]) · table entière supprimée (les 4 absents + toutes
|
||||||
|
lignes INTROUVABLES) · **artefact** réaffecte toutes les entrées `entite`→
|
||||||
|
`none_consolidated` (README périmé sur mécanisme ET verdict) · **artefact** rend
|
||||||
|
une entrée `own` porteuse d'un template (« Template émis » NON uniforme
|
||||||
|
[False,True] · invariant SSI cassé) ; restauré = green : 4 portées · table ==
|
||||||
|
artefact · par-ligne exact · fonction/uniforme/SSI verts · exit 0. Working tree
|
||||||
|
byte-restauré (`git checkout --`, **JAMAIS** `git clean`) · **7 gates re-verts**.
|
||||||
|
|
||||||
|
- `ci/README.md` (table récap du pipeline + paragraphe détaillé « 2ᵉ surface
|
||||||
|
`rbac/userperm_gen` ») mis à jour.
|
||||||
|
- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ;
|
||||||
|
édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact).
|
||||||
|
- **Auto-score 4Big** : 96/100.
|
||||||
+23
-2
File diff suppressed because one or more lines are too long
@@ -2959,6 +2959,125 @@ if wfv_roles is not None:
|
|||||||
f"« {row.strip()} » MAIS workflow.json dit garde « {exp} » → "
|
f"« {row.strip()} » MAIS workflow.json dit garde « {exp} » → "
|
||||||
f"réaffectation de privilège non répercutée, régénérer")
|
f"réaffectation de privilège non répercutée, régénérer")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# RBAC/userperm_gen · la TABLE « Mapping scope_donnees → mécanisme » — la
|
||||||
|
# FONCTION d'enforcement ROW-LEVEL : quel mécanisme Frappe natif applique CHAQUE
|
||||||
|
# portée de données, ET si un `User Permission` template est émis. CŒUR sécurité
|
||||||
|
# du module, transcrit À LA MAIN sans AUCUN gate d'IDENTITÉ.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Le bloc RBAC existant (« 3 volets ») ne gate QUE la VENTILATION par mécanisme
|
||||||
|
# (« 28 entite · 16 groupe · 2 own · 4 equipe » — un COMPTE, aveugle à QUEL
|
||||||
|
# mécanisme applique QUELLE portée). Restait, dans le MÊME README (l.33-38), une
|
||||||
|
# surface data-derived distincte : la table qui énonce, PAR portée, le `mechanism`
|
||||||
|
# Frappe natif ET le verdict « Template émis ? ». Source faisant autorité
|
||||||
|
# (byte-gatée par check_artifacts) : rbac/userperm_gen/out/user_permission_plan.json
|
||||||
|
# — chaque entrée porte `scope_donnees` + `mechanism` + `user_permission_template`
|
||||||
|
# (null ou objet). Le mapping est une FONCTION : chaque portée → exactement un
|
||||||
|
# mécanisme, template émis SSI `entite`/`user_permission_company`. RÉAFFECTER une
|
||||||
|
# portée à un mécanisme plus permissif (`entite` → `none_consolidated` : le
|
||||||
|
# row-level enforcement ABANDONNÉ, sur-exposition des données inter-entités — la
|
||||||
|
# restriction même que le module pose), RENOMMER un mécanisme ou basculer le
|
||||||
|
# verdict « Template émis ? » laisse le README périmé pendant que l'artefact dit
|
||||||
|
# autre chose → l'agent ERPNext câblerait le mauvais mécanisme (le risque même que
|
||||||
|
# la table veut prévenir). « Vert trompeur » qu'aucune suite tests/ (qui teste des
|
||||||
|
# FONCTIONS de mapping/résolution, pas la prose) n'attrape — même patron
|
||||||
|
# d'IDENTITÉ que la cross-cohérence PERMISSIONS Legal/CONFOTUR ou le catalogue RBAC
|
||||||
|
# fixtures_gen.
|
||||||
|
# Bonus (mordent un plan INTERNEMENT incohérent) : chaque portée mappe UN SEUL
|
||||||
|
# mécanisme (fonction, pas relation) · « Template émis » UNIFORME sur les entrées
|
||||||
|
# d'une portée · template émis EXACTEMENT pour `user_permission_company` (l'invariant
|
||||||
|
# « template SSI entite » du README:36/61) · ensemble des portées NON VIDE.
|
||||||
|
# État courant : aucune ligne périmée (anti-invention #6) — le défaut est la surface
|
||||||
|
# ungated. Un claim absent échoue AUSSI (traçabilité).
|
||||||
|
# ============================================================================
|
||||||
|
try:
|
||||||
|
up_plan = load("rbac/userperm_gen/out/user_permission_plan.json")
|
||||||
|
up_scopes, up_tmpl = {}, {} # portée -> set(mécanisme) / set(template non-null?)
|
||||||
|
for e in up_plan:
|
||||||
|
s = e["scope_donnees"]
|
||||||
|
up_scopes.setdefault(s, set()).add(e["mechanism"])
|
||||||
|
up_tmpl.setdefault(s, set()).add(e["user_permission_template"] is not None)
|
||||||
|
except (OSError, KeyError, ValueError, TypeError) as e:
|
||||||
|
bad(f"RBAC userperm map · user_permission_plan.json illisible/inattendu : {e}")
|
||||||
|
up_scopes = None
|
||||||
|
if up_scopes is not None:
|
||||||
|
# bonus 1 : ensemble des portées non vide
|
||||||
|
if not up_scopes:
|
||||||
|
bad("RBAC userperm map · plan VIDE (aucune portée) → contrat cassé")
|
||||||
|
else:
|
||||||
|
good(f"RBAC userperm map · {len(up_scopes)} portées dans le plan (ensemble non vide)")
|
||||||
|
# bonus 2 : chaque portée → UN SEUL mécanisme (fonction) + « Template émis » uniforme
|
||||||
|
scope_mech = {}
|
||||||
|
for s in sorted(up_scopes):
|
||||||
|
if len(up_scopes[s]) != 1:
|
||||||
|
bad(f"RBAC userperm map · portée « {s} » mappe {sorted(up_scopes[s])} "
|
||||||
|
f"(attendu 1 mécanisme) → plan incohérent (relation, pas fonction)")
|
||||||
|
continue
|
||||||
|
scope_mech[s] = next(iter(up_scopes[s]))
|
||||||
|
if len(up_tmpl[s]) != 1:
|
||||||
|
bad(f"RBAC userperm map · portée « {s} » — « Template émis ? » NON uniforme "
|
||||||
|
f"sur ses entrées {sorted(up_tmpl[s])} → plan incohérent")
|
||||||
|
# bonus 3 : template émis EXACTEMENT pour user_permission_company (l'invariant SSI)
|
||||||
|
for s, mech in scope_mech.items():
|
||||||
|
emitted = up_tmpl[s] == {True}
|
||||||
|
if emitted != (mech == "user_permission_company"):
|
||||||
|
bad(f"RBAC userperm map · portée « {s} » (mécanisme={mech}) émet "
|
||||||
|
f"template={emitted} MAIS l'invariant est « template SSI "
|
||||||
|
f"user_permission_company » → plan incohérent")
|
||||||
|
# identité + par-ligne vs README (lignes RAW : la table Mapping est la seule à 3
|
||||||
|
# cellules dont col1 = une portée backtickée `[a-z_]+` et col3 = le verdict
|
||||||
|
# « Template émis ? » **oui** / non (`null`)).
|
||||||
|
UPM_RD = f"{D}/rbac/userperm_gen/README.md"
|
||||||
|
try:
|
||||||
|
upm_lines = open(UPM_RD, encoding="utf-8").read().splitlines()
|
||||||
|
except OSError as e:
|
||||||
|
bad(f"RBAC userperm map · {UPM_RD} illisible : {e}"); upm_lines = []
|
||||||
|
map_rows = {}
|
||||||
|
for ln in upm_lines:
|
||||||
|
cells = [c.strip() for c in ln.strip().strip("|").split("|")]
|
||||||
|
if len(cells) != 3:
|
||||||
|
continue
|
||||||
|
ms = re.fullmatch(r"`([a-z_]+)`", cells[0])
|
||||||
|
if ms and re.search(r"\*\*oui\*\*|non \(`null`\)", cells[2]):
|
||||||
|
map_rows[ms.group(1)] = cells
|
||||||
|
# identité d'ensemble : portées de la table == portées de l'artefact
|
||||||
|
tbl, art = set(map_rows), set(scope_mech)
|
||||||
|
if tbl == art:
|
||||||
|
good(f"RBAC userperm map · table {sorted(tbl)} == portées de l'artefact "
|
||||||
|
f"({len(art)} · ni fantôme ni manquante)")
|
||||||
|
else:
|
||||||
|
miss = sorted(art - tbl); extra = sorted(tbl - art)
|
||||||
|
detail = (f" · absents={miss}" if miss else "") + \
|
||||||
|
(f" · en trop={extra}" if extra else "")
|
||||||
|
bad(f"RBAC userperm map · {UPM_RD} table {sorted(tbl)} MAIS artefact "
|
||||||
|
f"{sorted(art)}{detail} → régénérer le README")
|
||||||
|
# par portée : mécanisme (1er token backtické de col2) + verdict « Template émis ? »
|
||||||
|
for s in sorted(art):
|
||||||
|
exp_mech = scope_mech[s]
|
||||||
|
exp_emit = up_tmpl[s] == {True}
|
||||||
|
row = map_rows.get(s)
|
||||||
|
if row is None:
|
||||||
|
bad(f"RBAC userperm map · portée « {s} » — ligne INTROUVABLE dans {UPM_RD} "
|
||||||
|
f"(attendu mécanisme={exp_mech} · template="
|
||||||
|
f"{'oui' if exp_emit else 'non'})")
|
||||||
|
continue
|
||||||
|
mm = re.match(r"`([^`]+)`", row[1])
|
||||||
|
got_mech = mm.group(1) if mm else "?"
|
||||||
|
if got_mech == exp_mech:
|
||||||
|
good(f"RBAC userperm map · portée « {s} » — mécanisme « {got_mech} » == artefact")
|
||||||
|
else:
|
||||||
|
bad(f"RBAC userperm map · portée « {s} » — {UPM_RD} dit mécanisme "
|
||||||
|
f"« {got_mech} » MAIS artefact dit « {exp_mech} » → réaffectation "
|
||||||
|
f"d'enforcement non répercutée, régénérer")
|
||||||
|
got_emit = row[2].strip() == "**oui**"
|
||||||
|
if got_emit == exp_emit:
|
||||||
|
good(f"RBAC userperm map · portée « {s} » — Template émis="
|
||||||
|
f"{'oui' if got_emit else 'non'} == artefact")
|
||||||
|
else:
|
||||||
|
bad(f"RBAC userperm map · portée « {s} » — {UPM_RD} dit Template émis="
|
||||||
|
f"{'oui' if got_emit else 'non'} MAIS artefact dit "
|
||||||
|
f"{'oui' if exp_emit else 'non'} → régénérer le README")
|
||||||
|
|
||||||
sys.exit(1 if FAIL else 0)
|
sys.exit(1 if FAIL else 0)
|
||||||
PY
|
PY
|
||||||
rc=$?
|
rc=$?
|
||||||
|
|||||||
Reference in New Issue
Block a user