[FIX] migration: la sonde de visibilité déclare ce qu'elle n'a pas prouvé
Elle éprouvait 45 modèles sur 125 portant une règle globale, contre quatre utilisateurs internes — et les quatre étaient administrateurs. Un modèle que seuls les administrateurs peuvent lire passait donc au vert : c'est exactement la forme du bug DMS qui a motivé cet outil, vue d'un autre angle. Elle ne peut pas créer un témoin ordinaire — ce serait une écriture. Elle peut dire qu'elle n'en avait pas, annoncer sa couverture au lieu du seul nombre éprouvé, et déclarer qu'un masquage codé en Python lui échappe puisqu'elle part d'ir_rule. Un vert qui prouve moins qu'il n'en a l'air est pire qu'un rouge. --- EN --- It tested 45 models out of the 125 carrying a global rule, against four internal users — and all four were administrators. A model only admins can read therefore passed green: exactly the shape of the DMS bug that motivated this tool, seen from another angle. It cannot create an ordinary witness — that would be a write. It can say it had none, announce its coverage rather than only the number checked, and declare that masking coded in Python escapes it since it starts from ir_rule. A green that proves less than it looks is worse than a red. Assisted-by: Claude Opus 5
This commit is contained in:
parent
65a8a351a9
commit
2ba64132ca
3 changed files with 135 additions and 1 deletions
|
|
@ -91,6 +91,14 @@ try:
|
||||||
("share", "=", False),
|
("share", "=", False),
|
||||||
], limit=LIMITE)
|
], limit=LIMITE)
|
||||||
rapport["users"] = membres.mapped("login")
|
rapport["users"] = membres.mapped("login")
|
||||||
|
# Les témoins ORDINAIRES : ceux qui ne sont pas administrateurs.
|
||||||
|
# Un modèle que seuls les administrateurs peuvent lire passe au vert
|
||||||
|
# si tous les témoins en sont — c'est exactement la forme du bug DMS,
|
||||||
|
# vue d'un autre angle. Mesuré sur une base réelle : les QUATRE
|
||||||
|
# utilisateurs internes étaient membres de base.group_system.
|
||||||
|
rapport["ordinary"] = [
|
||||||
|
u.login for u in membres if not u.has_group("base.group_system")
|
||||||
|
]
|
||||||
if not membres:
|
if not membres:
|
||||||
rapport["no_user"] = True
|
rapport["no_user"] = True
|
||||||
else:
|
else:
|
||||||
|
|
@ -99,6 +107,10 @@ try:
|
||||||
.search([("global", "=", True), ("active", "=", True)])
|
.search([("global", "=", True), ("active", "=", True)])
|
||||||
.mapped("model_id.model")
|
.mapped("model_id.model")
|
||||||
)
|
)
|
||||||
|
# La COUVERTURE : combien de modèles portent une règle globale,
|
||||||
|
# et non combien on a pu éprouver. « 45 éprouvés » sans dire
|
||||||
|
# « sur 125 » laisse croire à un examen complet.
|
||||||
|
rapport["with_rule"] = len(vus - ATTENDUS)
|
||||||
for nom in sorted(vus - ATTENDUS):
|
for nom in sorted(vus - ATTENDUS):
|
||||||
modele = env.get(nom)
|
modele = env.get(nom)
|
||||||
if modele is None or modele._abstract or modele._transient:
|
if modele is None or modele._abstract or modele._transient:
|
||||||
|
|
@ -141,12 +153,24 @@ def render(rapport):
|
||||||
if rapport.get("no_user"):
|
if rapport.get("no_user"):
|
||||||
return [f"⚠ {t('No internal user to test visibility with.')}"]
|
return [f"⚠ {t('No internal user to test visibility with.')}"]
|
||||||
muets = rapport.get("models") or []
|
muets = rapport.get("models") or []
|
||||||
|
porteurs = rapport.get("with_rule")
|
||||||
|
sur = f" {t('out of')} {porteurs}" if porteurs else ""
|
||||||
lignes = [
|
lignes = [
|
||||||
f"🔍 {rapport.get('checked', 0)}"
|
f"🔍 {rapport.get('checked', 0)}{sur}"
|
||||||
f" {t('model(s) with a global rule and some data,')}"
|
f" {t('model(s) with a global rule and some data,')}"
|
||||||
f" {t('checked against')} {len(rapport.get('users') or [])}"
|
f" {t('checked against')} {len(rapport.get('users') or [])}"
|
||||||
f" {t('internal user(s)')}"
|
f" {t('internal user(s)')}"
|
||||||
]
|
]
|
||||||
|
# Dire ce qu'on n'a PAS pu éprouver, avant de dire ce qu'on a trouvé.
|
||||||
|
# Un vert qui prouve moins qu'il n'en a l'air est pire qu'un rouge.
|
||||||
|
if "ordinary" in rapport and not rapport["ordinary"]:
|
||||||
|
lignes.append(
|
||||||
|
f" ⚠ {t('Every witness is an administrator: a model only')}"
|
||||||
|
f" {t('admins can read passes this test.')}"
|
||||||
|
)
|
||||||
|
lignes.append(
|
||||||
|
f" ℹ {t('Reads ir_rule only — masking coded in Python escapes it.')}"
|
||||||
|
)
|
||||||
if not muets:
|
if not muets:
|
||||||
lignes.append(f" ✅ {t('Every one of them is visible to someone.')}")
|
lignes.append(f" ✅ {t('Every one of them is visible to someone.')}")
|
||||||
return lignes
|
return lignes
|
||||||
|
|
|
||||||
|
|
@ -6789,6 +6789,22 @@ TRANSLATIONS = {
|
||||||
"fr": "Aucun index en double.",
|
"fr": "Aucun index en double.",
|
||||||
"en": "No duplicate index.",
|
"en": "No duplicate index.",
|
||||||
},
|
},
|
||||||
|
"out of": {
|
||||||
|
"fr": "sur",
|
||||||
|
"en": "out of",
|
||||||
|
},
|
||||||
|
"Every witness is an administrator: a model only": {
|
||||||
|
"fr": "Tous les témoins sont administrateurs : un modèle que seuls les",
|
||||||
|
"en": "Every witness is an administrator: a model only",
|
||||||
|
},
|
||||||
|
"admins can read passes this test.": {
|
||||||
|
"fr": "administrateurs peuvent lire passe ce test.",
|
||||||
|
"en": "admins can read passes this test.",
|
||||||
|
},
|
||||||
|
"Reads ir_rule only — masking coded in Python escapes it.": {
|
||||||
|
"fr": "Ne lit qu'ir_rule — un masquage codé en Python lui échappe.",
|
||||||
|
"en": "Reads ir_rule only — masking coded in Python escapes it.",
|
||||||
|
},
|
||||||
"held no data of their own — nothing to lose": {
|
"held no data of their own — nothing to lose": {
|
||||||
"fr": "sans donnée propre — rien à perdre",
|
"fr": "sans donnée propre — rien à perdre",
|
||||||
"en": "held no data of their own — nothing to lose",
|
"en": "held no data of their own — nothing to lose",
|
||||||
|
|
|
||||||
|
|
@ -237,5 +237,99 @@ class TestTheWiring(unittest.TestCase):
|
||||||
self.assertIn("--apply", src[debut : debut + 200])
|
self.assertIn("--apply", src[debut : debut + 200])
|
||||||
|
|
||||||
|
|
||||||
|
class TestItDeclaresWhatItCouldNotProve(unittest.TestCase):
|
||||||
|
"""Un vert qui prouve moins qu'il n'en a l'air est pire qu'un rouge.
|
||||||
|
|
||||||
|
Mesuré sur une base réelle : l'outil éprouvait 45 modèles sur 125
|
||||||
|
portant une règle globale, contre 4 utilisateurs internes — et les
|
||||||
|
QUATRE étaient administrateurs. Un modèle que seuls les
|
||||||
|
administrateurs peuvent lire passait donc au vert. C'est exactement
|
||||||
|
la forme du bug DMS qui a motivé l'outil, vue d'un autre angle.
|
||||||
|
|
||||||
|
L'outil ne peut pas créer un témoin ordinaire — ce serait une
|
||||||
|
écriture. Il peut dire qu'il n'en avait pas.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def rapport(self, **extra):
|
||||||
|
base = {
|
||||||
|
"checked": 45,
|
||||||
|
"with_rule": 125,
|
||||||
|
"users": ["a", "b", "c", "d"],
|
||||||
|
"ordinary": ["b"],
|
||||||
|
"models": [],
|
||||||
|
}
|
||||||
|
base.update(extra)
|
||||||
|
return base
|
||||||
|
|
||||||
|
def tete(self, **extra):
|
||||||
|
return "\n".join(check.render(self.rapport(**extra)))
|
||||||
|
|
||||||
|
def test_it_says_how_many_it_could_not_check(self):
|
||||||
|
# « 45 éprouvés » sans dire « sur 125 » laisse croire à un examen
|
||||||
|
# complet.
|
||||||
|
texte = self.tete()
|
||||||
|
self.assertIn("45", texte)
|
||||||
|
self.assertIn("125", texte)
|
||||||
|
|
||||||
|
def test_without_the_coverage_it_says_only_what_it_checked(self):
|
||||||
|
# Un rapport d'une version antérieure n'a pas le renseignement.
|
||||||
|
# Inventer « sur 0 » serait pire que se taire — et « sur None »
|
||||||
|
# pire encore : c'est ce que produit un f-string sans garde.
|
||||||
|
texte = self.tete(with_rule=None)
|
||||||
|
self.assertIn("45", texte)
|
||||||
|
self.assertNotIn("125", texte)
|
||||||
|
self.assertNotIn("None", texte)
|
||||||
|
self.assertNotIn(check.t("out of"), texte.split("\n")[0])
|
||||||
|
|
||||||
|
def test_it_warns_when_every_witness_is_an_administrator(self):
|
||||||
|
texte = self.tete(ordinary=[])
|
||||||
|
self.assertIn(
|
||||||
|
check.t("Every witness is an administrator: a model only"), texte
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_it_stays_quiet_when_one_witness_is_ordinary(self):
|
||||||
|
self.assertNotIn(
|
||||||
|
check.t("Every witness is an administrator: a model only"),
|
||||||
|
self.tete(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_it_declares_what_it_cannot_see_at_all(self):
|
||||||
|
# La sonde part d'ir_rule : un masquage écrit en Python lui
|
||||||
|
# échappe par construction, et le taire ferait prendre son vert
|
||||||
|
# pour une garantie.
|
||||||
|
self.assertIn(
|
||||||
|
check.t(
|
||||||
|
"Reads ir_rule only — masking coded in Python escapes it."
|
||||||
|
),
|
||||||
|
self.tete(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_the_warning_comes_before_the_verdict(self):
|
||||||
|
lignes = check.render(self.rapport(ordinary=[]))
|
||||||
|
avertissement = next(
|
||||||
|
i
|
||||||
|
for i, x in enumerate(lignes)
|
||||||
|
if "administrator" in x or "administrateur" in x
|
||||||
|
)
|
||||||
|
verdict = next(i for i, x in enumerate(lignes) if "✅" in x)
|
||||||
|
self.assertLess(avertissement, verdict)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTheScriptCollectsWhatTheReportNeeds(unittest.TestCase):
|
||||||
|
def corps(self):
|
||||||
|
return check.build_script()
|
||||||
|
|
||||||
|
def test_it_counts_the_models_carrying_a_rule(self):
|
||||||
|
self.assertIn('rapport["with_rule"]', self.corps())
|
||||||
|
|
||||||
|
def test_it_separates_the_ordinary_witnesses(self):
|
||||||
|
corps = self.corps()
|
||||||
|
self.assertIn('rapport["ordinary"]', corps)
|
||||||
|
self.assertIn("base.group_system", corps)
|
||||||
|
|
||||||
|
def test_the_script_is_valid_python(self):
|
||||||
|
compile(self.corps(), "script", "exec")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue