preuves : trois gardes pour ce que ma lecture ne tiendra pas
Une revision de documentation vieillit comme le reste. Ce qui tient, c est ce
qu une machine verifie — et trois lacunes etaient nommees sans etre gardees.
P58 HABILITATIONS. autorisation.md posait la regle (un service nomme un
GROUPE, jamais une personne, D-66) et meta/acces.yml la portait ; rien ne
la verifiait. P29 gardait les POSITIONS d authentification, personne ne
gardait les DROITS.
Le controle qui porte la preuve est un croisement : une entree
porte_par: role-realm affirme que l habilitation voyage par un role de
realm projete depuis un groupe LDAP. P58 le confronte a serveur_keycloak.
Sans ca, un service annonce une habilitation que rien ne transporte, et l
ecran reste vide sans que personne sache pourquoi.
CE QU ELLE N EXIGE PAS, et c est le point le plus important : que les
groupes nommes existent dans l annuaire. Ce serait contredire le regime du
paragraphe 2 — le depot AMORCE un acces et se retire, les appartenances
appartiennent a une personne. dev et personnel n existent dans aucun code,
et ce n est pas un defaut.
P59 ENUMERATIONS ANNONCEES. Les deux ecarts trouves a la main pendant la
tournee — cinq portes annoncees devant une table de six, huit lignes
renvoyees vers une fiche qui en compte dix — etaient d une forme que P57
ne voit pas.
Ma premiere version a signale CINQ ecarts, et les cinq etaient du bruit :
dans « reprise dans les deux devis : », le nombre qualifie autre chose que
la liste. Cent pour cent de faux positifs — la preuve qui crie sur un cas
sain et qu on apprend a ignorer. Resserree aux deux formes ou le nombre ne
peut compter rien d autre. Etroite et vraie plutot que large et devineuse.
P60 WIKI PUBLIE. Le wiki est publie DEPUIS le depot ; rien ne mesurait l ecart,
et il s est creuse de VINGT-SEPT JOURS en silence. Deux unites jamais
publiees, vingt et une differentes : pour qui lit la forge plutot que le
depot, toute la revision n existait pas.
Le harnais est STATIQUE, zero appel reseau — cloner la forge romprait la
seule propriete qui fasse qu une preuve vaille hors de ce poste. La mesure
passe donc par un TEMOIN que make wiki-publier depose. Amorce avec la
valeur MESUREE : le wiki d eregion porte b6167f2, dont le message dit
source: ac85278.
Ce qu elle ne prouve pas : un temoin dit ce qui est PARTI, jamais ce qui
est ARRIVE.
LES TROIS SONT EPROUVEES DANS LES DEUX SENS
Douze essais negatifs, douze refus : groupe non projete, acces.yml disparu,
personne au lieu d un groupe, mecanisme invente, raison manquante, compte
revenu a cinq, septieme porte ajoutee sans toucher au compte, renvoi croise
fausse, temoin absent, temoin d un autre depot. Une garantie qu on n a jamais vu
dire non n est pas une garantie, c est une habitude.
ETAT : NON CONFORME, 58 OK, 1 echec, 1 saute.
P60 est rouge, et c est le comportement voulu : le registre a le droit de
perdre. Le retard qu elle signale est reel et anterieur a elle. Une commande le
ferme, et elle vient ensuite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
This commit is contained in:
parent
36b6926e21
commit
c9d31e9a60
13 changed files with 482 additions and 16 deletions
|
|
@ -184,7 +184,7 @@ Si `ansible-lint` n’est pas disponible, le signaler clairement. Ne pas invente
|
||||||
## Écrire, puis relire (D-68)
|
## Écrire, puis relire (D-68)
|
||||||
|
|
||||||
`--syntax-check` et `ansible-lint` prouvent que le dépôt est cohérent **avec lui-même**.
|
`--syntax-check` et `ansible-lint` prouvent que le dépôt est cohérent **avec lui-même**.
|
||||||
C'est aussi ce que font les 57 preuves de `make prouver` : elles lisent le dépôt, sans le
|
C'est aussi ce que font les 60 preuves de `make prouver` : elles lisent le dépôt, sans le
|
||||||
moindre appel réseau. **Aucune ne demande au système déployé s'il ressemble à ce que le
|
moindre appel réseau. **Aucune ne demande au système déployé s'il ressemble à ce que le
|
||||||
dépôt annonce.**
|
dépôt annonce.**
|
||||||
|
|
||||||
|
|
|
||||||
11
Makefile
11
Makefile
|
|
@ -833,7 +833,16 @@ wiki-publier: ## Publie le wiki (wiki/) vers la forge
|
||||||
sha="$$(git -C "$(CURDIR)" rev-parse --short HEAD 2>/dev/null || echo inconnu)"; \
|
sha="$$(git -C "$(CURDIR)" rev-parse --short HEAD 2>/dev/null || echo inconnu)"; \
|
||||||
git commit --quiet -m "Publication du wiki depuis le depot (source: $$sha)"; \
|
git commit --quiet -m "Publication du wiki depuis le depot (source: $$sha)"; \
|
||||||
git push --quiet; \
|
git push --quiet; \
|
||||||
printf '%s\n' 'Wiki publie.'
|
printf '%s\n' 'Wiki publie.'; \
|
||||||
|
@# LE TEMOIN — sans lui, l'ecart entre `wiki/` et la forge n'est mesurable par
|
||||||
|
@# personne. Il s'est creuse de 27 jours en silence avant que P60 n'existe.
|
||||||
|
@# Ecrit APRES le push : un temoin ne doit jamais dire qu'on a publie si on a echoue.
|
||||||
|
{ printf '%s\n' '---' \
|
||||||
|
'# Ecrit par `make wiki-publier`, lu par la preuve P60. Ne pas editer a la main.' \
|
||||||
|
"remote: $(WIKI_REMOTE)" \
|
||||||
|
"source: $$sha" \
|
||||||
|
"date: $$(date +%F)"; } > "$(CURDIR)/docs/audit/wiki-publie.yml"; \
|
||||||
|
printf '%s\n' 'Temoin depose : docs/audit/wiki-publie.yml (a committer).'
|
||||||
|
|
||||||
deployer-tout: _instance-requise ## Deploie TOUTE la flotte dans l'ordre des couches
|
deployer-tout: _instance-requise ## Deploie TOUTE la flotte dans l'ordre des couches
|
||||||
@set -e; \
|
@set -e; \
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ Ce plan est le **pendant manuel** de `make prouver` : là où le harnais prouve
|
||||||
le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (cf.
|
le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (cf.
|
||||||
`protocole-operateur-independant.md`).
|
`protocole-operateur-independant.md`).
|
||||||
|
|
||||||
**90 gestes** sur **22 unités** · **19** en « casse-répare »
|
**90 gestes** sur **22 unités** · **20** en « casse-répare »
|
||||||
· **5** doublés d'un garde-fou machine (colonne *Preuve auto*).
|
· **5** doublés d'un garde-fou machine (colonne *Preuve auto*).
|
||||||
|
|
||||||
> **Honnêteté de couverture.** La colonne *Preuve auto* n'est remplie que lorsqu'une
|
> **Honnêteté de couverture.** La colonne *Preuve auto* n'est remplie que lorsqu'une
|
||||||
|
|
@ -150,7 +150,7 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
|
||||||
| 1 | Lis ton réseau physique | : make underlay. Repère les VLAN sous 1000 (l'underlay) et les MTU. Pourquoi le transport doit-il être à 1500 quand l'overlay est à 1450 ? | 👁 observe | — |
|
| 1 | Lis ton réseau physique | : make underlay. Repère les VLAN sous 1000 (l'underlay) et les MTU. Pourquoi le transport doit-il être à 1500 quand l'overlay est à 1450 ? | 👁 observe | — |
|
||||||
| 2 | Regarde sans écrire | : make sdn-plan. Si le cluster dit déjà ce que le plan dit, la sortie tient en une ligne. | 👁 observe | — |
|
| 2 | Regarde sans écrire | : make sdn-plan. Si le cluster dit déjà ce que le plan dit, la sortie tient en une ligne. | 👁 observe | — |
|
||||||
| 3 | Trouve la sortie d'un tenant | : dans make devis-sdn, repère la strophe FRR et l'adresse du prochain saut. À quel équipement appartient-elle ? | 👁 observe | — |
|
| 3 | Trouve la sortie d'un tenant | : dans make devis-sdn, repère la strophe FRR et l'adresse du prochain saut. À quel équipement appartient-elle ? | 👁 observe | — |
|
||||||
| 4 | Change `index` dans un modèle | (jamais en production) et régénère : combien de valeurs ont bougé ? C'est la mesure exacte de ce que la dérivation t'épargne. Pour aller plus loin : docs/sdn-evpn.md (référence technique), Le plan & l'adressage dérivé, Multi-instance & f… | 👁 observe | — |
|
| 4 | Change `index` dans un modèle | (jamais en production) et régénère : combien de valeurs ont bougé ? C'est la mesure exacte de ce que la dérivation t'épargne. Pour aller plus loin : docs/sdn-evpn.md dans le dépôt (référence technique), Le plan & l'adressage dérivé, Mult… | 👁 observe | — |
|
||||||
|
|
||||||
*Source : [Le réseau des tenants — du câble au VRF](../../wiki/Le-r%C3%A9seau-des-tenants.md) · § À toi de jouer.*
|
*Source : [Le réseau des tenants — du câble au VRF](../../wiki/Le-r%C3%A9seau-des-tenants.md) · § À toi de jouer.*
|
||||||
|
|
||||||
|
|
|
||||||
96
docs/audit/preuve-2026-09-08.md
Normal file
96
docs/audit/preuve-2026-09-08.md
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
# Preuve de conformite — Set-OPS — 2026-09-08
|
||||||
|
|
||||||
|
> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer
|
||||||
|
> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ;
|
||||||
|
> aucune validation n'est reimplementee ici. Voir le mode d'emploi :
|
||||||
|
> [`docs/audit/README.md`](README.md), et le registre trace :
|
||||||
|
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||||
|
|
||||||
|
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/principal/hosts.yml`
|
||||||
|
- **Verdict** : ❌ NON CONFORME (58 OK · 1 echec · 1 saute)
|
||||||
|
|
||||||
|
## Preuves
|
||||||
|
|
||||||
|
| # | Preuve | Affirmations | Statut | Detail |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | [0m[0m |
|
||||||
|
| P02 | Tests unitaires (inventory_host) | — | ✅ OK | >>> le verrou tient : aucune VM n'aurait ete touchee |
|
||||||
|
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 4 instance(s) verifiee(s) — OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, OPS-Patient0 : plan et inventaire applique coincident. |
|
||||||
|
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
|
||||||
|
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
|
||||||
|
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. |
|
||||||
|
| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check). |
|
||||||
|
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 40 groupes classes, aucun cycle, aucune arete en arriere. |
|
||||||
|
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 38 rôles, 99 flux, schéma + matrice OK. |
|
||||||
|
| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). |
|
||||||
|
| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | serveur_resolveur_site |
|
||||||
|
| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
|
||||||
|
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
|
||||||
|
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
|
||||||
|
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
||||||
|
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). |
|
||||||
|
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
||||||
|
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 23 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
|
||||||
|
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 28 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
||||||
|
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. |
|
||||||
|
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 3 instance(s) federee(s), aucun index en collision. |
|
||||||
|
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
|
||||||
|
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 13 reseau(x), aucune collision avec la plage tenant. |
|
||||||
|
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
|
||||||
|
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 50 groupe(s), 80 regle(s). |
|
||||||
|
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 5 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
|
||||||
|
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
|
||||||
|
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 34 VM placee(s), aucun nom ni VMID en collision. |
|
||||||
|
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 32 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 21, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
|
||||||
|
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. |
|
||||||
|
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 57 scripts expliques et atteignables, 114 cibles make documentees, 65 roles avec README. |
|
||||||
|
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (131 cle(s) declaree(s) par l'instance). |
|
||||||
|
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 33 revendication(s) de port, aucune collision entre roles co-localises (37 groupes). |
|
||||||
|
| P34 | Chaque document declare son lecteur | — | ✅ OK | 43 document(s) declarent leur lecteur (34 genere(s) exempte(s)). |
|
||||||
|
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). |
|
||||||
|
| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) de l'ecosysteme et 3 du site detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). |
|
||||||
|
| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. |
|
||||||
|
| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 39 role(s) serveur/client tous nommes, 40 groupe(s) cite(s) en table existent tous. |
|
||||||
|
| P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 27 page(s) de wiki toutes atteignables. |
|
||||||
|
| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
|
||||||
|
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 53 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
||||||
|
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal, OPS-Patient0/product |
|
||||||
|
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 7 machine(s) du plan retrouvees, 104 regle(s) du site. |
|
||||||
|
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 4 integration(s) appliquent leur serveur avant leurs clients. |
|
||||||
|
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
|
||||||
|
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
|
||||||
|
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
|
||||||
|
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 87 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
|
||||||
|
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (117 lignes). |
|
||||||
|
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 169 regles `pass`), tous non consignes et tous motives. |
|
||||||
|
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
|
||||||
|
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
|
||||||
|
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 15 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b |
|
||||||
|
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 10 role(s) applique(s), aucun secret de tenant reclame. |
|
||||||
|
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 14 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 14. |
|
||||||
|
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
|
||||||
|
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (60 preuves, 65 roles, 40 groupes). |
|
||||||
|
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
|
||||||
|
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
|
||||||
|
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ❌ ECHEC | Le wiki en ligne est EN RETARD : 9 commit(s) ont touche `wiki/` depuis la publication de `ac85278` (2026-08-10), le dernier etant 36b6926 2026-09-07. Ce que la |
|
||||||
|
|
||||||
|
## Couverture des affirmations ✅ du registre
|
||||||
|
|
||||||
|
Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus.
|
||||||
|
Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005
|
||||||
|
`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite
|
||||||
|
d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ;
|
||||||
|
elles restent hors du harnais recurrent (rien d'executable a rejouer).
|
||||||
|
|
||||||
|
## Declarations d'intention (⚪ invérifiables localement — assumees)
|
||||||
|
|
||||||
|
Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees**
|
||||||
|
comme declarations d'intention, non comme preuves :
|
||||||
|
|
||||||
|
- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement
|
||||||
|
contre une flotte vivante.
|
||||||
|
- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles.
|
||||||
|
- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee.
|
||||||
|
- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume.
|
||||||
|
|
||||||
|
_Rapport genere le 2026-09-08._
|
||||||
18
docs/audit/wiki-publie.yml
Normal file
18
docs/audit/wiki-publie.yml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
# TEMOIN DE PUBLICATION DU WIKI — ecrit par `make wiki-publier`, lu par la preuve P60.
|
||||||
|
#
|
||||||
|
# POURQUOI UN TEMOIN, ET PAS UNE MESURE DIRECTE (2026-09-07). Le harnais est STATIQUE :
|
||||||
|
# il lit le depot, sans un seul appel reseau, et c'est ce qui le rend rejouable partout et
|
||||||
|
# par n'importe qui. Cloner le wiki de la forge pour le comparer romprait cette propriete
|
||||||
|
# — la seule qui fait qu'une preuve vaut quelque chose hors du poste de l'exploitant.
|
||||||
|
#
|
||||||
|
# On enregistre donc, A LA PUBLICATION, le commit du depot qui a ete publie. P60 compare
|
||||||
|
# ensuite ce commit a l'etat de `wiki/` : si le dossier a bouge depuis, le wiki en ligne
|
||||||
|
# est en retard, et elle le dit. Meme discipline que `docs/audit/preuve-<date>.md`.
|
||||||
|
#
|
||||||
|
# VALEUR D'AMORCAGE, MESUREE et non supposee : le wiki d'eregion portait le commit
|
||||||
|
# b6167f2, dont le message est « Publication du wiki depuis le depot (source: ac85278) ».
|
||||||
|
# C'est donc ac85278 qui a ete publie, le 2026-08-10.
|
||||||
|
remote: ssh://git@eregion.chezlepro.ca:2222/Alliance-Boreale/Set-OPS-Public.wiki.git
|
||||||
|
source: ac85278
|
||||||
|
date: 2026-08-10
|
||||||
|
|
@ -105,6 +105,10 @@ inventée, un `web-sso` sans accès de secours ou sans posture de formulaire, un
|
||||||
natif` sans réglage `<rôle>_connexion_locale` **défini dans `defaults`**, et une
|
natif` sans réglage `<rôle>_connexion_locale` **défini dans `defaults`**, et une
|
||||||
déclaration que le code contredit.
|
déclaration que le code contredit.
|
||||||
|
|
||||||
|
**Et depuis le 2026-09-07, sa sœur `P58` garde les habilitations** — ce que P29 ne fait
|
||||||
|
pas : elle tient les *positions* (qui s'authentifie comment), pas les *droits* (qui obtient
|
||||||
|
quoi). Voir [`autorisation.md`](autorisation.md) §5.
|
||||||
|
|
||||||
**Depuis le 2026-09-06, elle refuse aussi que ce tableau mente.** Les chiffres et les listes
|
**Depuis le 2026-09-06, elle refuse aussi que ce tableau mente.** Les chiffres et les listes
|
||||||
de la troisième colonne sont confrontés aux déclarations réelles. Ils avaient cessé d'être
|
de la troisième colonne sont confrontés aux déclarations réelles. Ils avaient cessé d'être
|
||||||
vrais sans que rien ne le signale : la ligne `sans-auth-humaine` annonçait 12 rôles, il y en
|
vrais sans que rien ne le signale : la ligne `sans-auth-humaine` annonçait 12 rôles, il y en
|
||||||
|
|
|
||||||
|
|
@ -403,11 +403,24 @@ en fonctionnement. L'état réel :
|
||||||
|---|---|
|
|---|---|
|
||||||
| Le rôle d'amorçage | **`roles/amorcage_acces/`** — écrit, déployé, et c'est lui qui crée l'unique compte `sysadmin` du §3 |
|
| Le rôle d'amorçage | **`roles/amorcage_acces/`** — écrit, déployé, et c'est lui qui crée l'unique compte `sysadmin` du §3 |
|
||||||
| Les `meta/acces.yml` | **écrits pour les cinq services `web-sso`** : `serveur_forgejo`, `serveur_grafana`, `serveur_icingaweb2`, `serveur_keycloak`, `serveur_nextcloud` |
|
| Les `meta/acces.yml` | **écrits pour les cinq services `web-sso`** : `serveur_forgejo`, `serveur_grafana`, `serveur_icingaweb2`, `serveur_keycloak`, `serveur_nextcloud` |
|
||||||
| La preuve | **toujours à écrire** — c'est la seule ligne d'origine qui tient |
|
| La preuve | **écrite le 2026-09-07 — c'est P58** |
|
||||||
|
|
||||||
La lacune restante mérite d'être nommée pour ce qu'elle est. Le §5 de
|
Le §5 de [`authentification.md`](authentification.md) formule la règle : *une directive
|
||||||
[`authentification.md`](authentification.md) le formule ainsi : *une directive qu'aucune
|
qu'aucune garde ne vérifie finit par ne plus être vraie*. `meta/acces.yml` a été dans ce cas
|
||||||
garde ne vérifie finit par ne plus être vraie*. `meta/acces.yml` est exactement dans ce cas
|
jusqu'au 2026-09-07 : rien ne vérifiait qu'un service `web-sso` en porte un. **P29** gardait
|
||||||
— rien ne vérifie qu'un service `web-sso` en porte un, ni que le groupe qu'il nomme existe
|
les *positions* d'authentification ; **P58** garde désormais les *habilitations*.
|
||||||
dans l'annuaire. **P29** garde les *positions* d'authentification ; personne ne garde encore
|
|
||||||
les *habilitations*.
|
Ce qu'elle exige :
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| tout rôle `web-sso` porte un `meta/acces.yml` | **sauf** `formulaire_local: aucun` — une passerelle authentifie devant, elle n'accorde rien. L'exemption est **dérivée** de la déclaration, jamais un nom en dur |
|
||||||
|
| chaque entrée nomme `groupe`, `accorde`, `porte_par`, `raison` | `porte_par` existe pour rendre visible, dans la déclaration même, **ce qui n'est pas câblé** |
|
||||||
|
| `groupe` est un groupe, pas un compte | ni `@`, ni `uid=` — c'est D-66, tenu par une machine |
|
||||||
|
| une habilitation `porte_par: role-realm` est **réellement projetée** par `serveur_keycloak` | c'est le croisement qui porte la preuve : sans lui, un service peut annoncer une habilitation que rien ne transporte, et l'écran reste vide sans que personne sache pourquoi |
|
||||||
|
|
||||||
|
> **Ce qu'elle n'exige PAS, et c'est le point le plus important.** Elle ne demande pas que
|
||||||
|
> les groupes nommés existent dans l'annuaire. Ce serait contredire le §2 : le dépôt
|
||||||
|
> **amorce** un accès et se retire ; les appartenances appartiennent à une personne.
|
||||||
|
> `amorcage_acces` ne crée qu'un groupe — `dev` et `personnel` sont créés par l'exploitant,
|
||||||
|
> et leur absence du code n'est pas un défaut, c'est le régime.
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ README de rôles). Cette page comble ces deux trous.
|
||||||
| rôles | 65 | `roles/*/` |
|
| rôles | 65 | `roles/*/` |
|
||||||
| README de rôles | 65 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette |
|
| README de rôles | 65 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette |
|
||||||
| documents | 39 | `docs/*.md` |
|
| documents | 39 | `docs/*.md` |
|
||||||
| pièces d'audit | 36 | `docs/audit/*` |
|
| pièces d'audit | 38 | `docs/audit/*` |
|
||||||
| unités de wiki | 27 | `wiki/*.md` |
|
| unités de wiki | 27 | `wiki/*.md` |
|
||||||
| décisions en vigueur | 80 | lignes `\| **D-nn** \|` de `decisions-architecture.md` |
|
| décisions en vigueur | 80 | lignes `\| **D-nn** \|` de `decisions-architecture.md` |
|
||||||
| décisions renversées | 3 | lignes `\| **D-nn** —` du même document |
|
| décisions renversées | 3 | lignes `\| **D-nn** —` du même document |
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ make placement-plan # chaque VM est-elle là où le plan la met
|
||||||
|
|
||||||
## Le trou qu'il comble
|
## Le trou qu'il comble
|
||||||
|
|
||||||
`scripts/prouver.py` porte 57 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
`scripts/prouver.py` porte 60 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
||||||
dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est
|
dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est
|
||||||
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
|
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
|
||||||
propriétaire, que rien n'est codé en dur.
|
propriétaire, que rien n'est codé en dur.
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,311 @@ def preuve_modele_socle() -> tuple[bool, str]:
|
||||||
PORTEES_AUTH = {"web-sso", "socle-identite", "ldap-direct", "interne-sans-auth",
|
PORTEES_AUTH = {"web-sso", "socle-identite", "ldap-direct", "interne-sans-auth",
|
||||||
"sans-auth-humaine"}
|
"sans-auth-humaine"}
|
||||||
|
|
||||||
|
# LES MECANISMES QUI PORTENT REELLEMENT UNE HABILITATION (meta/acces.yml, champ
|
||||||
|
# `porte_par`). Le champ existe pour rendre visible, DANS LA DECLARATION, ce qui n'est
|
||||||
|
# pas cable : sans lui, « le groupe sysadmin donne Admin » se lit comme un fait alors que
|
||||||
|
# c'est parfois un souhait. Un mecanisme hors de ce jeu est donc refuse — inventer un nom
|
||||||
|
# de transport, c'est se donner l'air d'avoir cable quelque chose.
|
||||||
|
MECANISMES_ACCES = {
|
||||||
|
"defaut", # tout utilisateur authentifie l'obtient, sans rien porter
|
||||||
|
"claim-groupe", # le claim `groups` du jeton, lu par l'application
|
||||||
|
"role-realm", # un role de realm Keycloak, projete depuis un groupe LDAP
|
||||||
|
"role-client", # un role d'un client OIDC (ex. realm-management)
|
||||||
|
"groupe-annuaire", # l'application lit l'annuaire directement
|
||||||
|
"group-ldap-mapper", # le mapper Keycloak qui projette un groupe en role
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
NOMBRES_FR = {"deux": 2, "trois": 3, "quatre": 4, "cinq": 5, "six": 6, "sept": 7,
|
||||||
|
"huit": 8, "neuf": 9, "dix": 10, "onze": 11, "douze": 12, "treize": 13,
|
||||||
|
"quatorze": 14, "quinze": 15, "seize": 16}
|
||||||
|
|
||||||
|
|
||||||
|
def _enumeration_apres(lignes: list[str], i: int) -> tuple[str | None, int]:
|
||||||
|
"""Le bloc qui suit la ligne `i` est-il une table ou une liste, et de combien ?"""
|
||||||
|
j = i + 1
|
||||||
|
while j < len(lignes) and not lignes[j].strip():
|
||||||
|
j += 1
|
||||||
|
if j >= len(lignes):
|
||||||
|
return None, 0
|
||||||
|
s = lignes[j].lstrip()
|
||||||
|
if s.startswith("|"):
|
||||||
|
k = j + 2 if j + 1 < len(lignes) and set(lignes[j + 1].strip()) <= set("|-: ") else j
|
||||||
|
n = 0
|
||||||
|
while k < len(lignes) and lignes[k].lstrip().startswith("|"):
|
||||||
|
n += 1
|
||||||
|
k += 1
|
||||||
|
return "table", n
|
||||||
|
if re.match(r"^([-*+]|\d+\.)\s", s):
|
||||||
|
n, k = 0, j
|
||||||
|
while k < len(lignes):
|
||||||
|
u = lignes[k].lstrip()
|
||||||
|
if re.match(r"^([-*+]|\d+\.)\s", u):
|
||||||
|
n += 1
|
||||||
|
elif not lignes[k].strip() or lignes[k][:1] in " \t":
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
k += 1
|
||||||
|
return "liste", n
|
||||||
|
return None, 0
|
||||||
|
|
||||||
|
|
||||||
|
def _lignes_premiere_table(texte: str) -> int:
|
||||||
|
"""Lignes de DONNEES de la PREMIERE table markdown d'un texte.
|
||||||
|
|
||||||
|
« Premiere », et c'est une correction de l'instrument, pas de la cible : le §7 de
|
||||||
|
`preparer-un-site-hebergeur.md` porte DEUX tables. En les additionnant, la garde
|
||||||
|
annoncait un ecart la ou il n'y en avait pas — elle accusait le document de ce
|
||||||
|
qu'elle mesurait mal.
|
||||||
|
"""
|
||||||
|
n, dans = 0, False
|
||||||
|
for l in texte.splitlines():
|
||||||
|
s = l.lstrip()
|
||||||
|
if s.startswith("|"):
|
||||||
|
if not dans:
|
||||||
|
dans = True # l'en-tete
|
||||||
|
continue
|
||||||
|
if set(s) <= set("|-: "):
|
||||||
|
continue # le separateur
|
||||||
|
n += 1
|
||||||
|
elif dans:
|
||||||
|
break
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def preuve_wiki_publie_a_jour() -> tuple[bool, str]:
|
||||||
|
"""Le wiki publie sur la forge correspond a `wiki/` dans le depot.
|
||||||
|
|
||||||
|
LA LACUNE QUE CETTE PREUVE COMBLE (2026-09-07). Le wiki est publie DEPUIS le depot :
|
||||||
|
« on lit la-bas, on ecrit ici ». Rien ne mesurait l'ecart entre les deux, et il s'est
|
||||||
|
creuse de VINGT-SEPT JOURS sans que personne ne le voie — deux unites n'avaient jamais
|
||||||
|
ete publiees, vingt et une differaient. Pour qui lit le wiki plutot que le depot,
|
||||||
|
toute une revision de documentation n'existait pas.
|
||||||
|
|
||||||
|
POURQUOI UN TEMOIN PLUTOT QUE LA MESURE DIRECTE. Le harnais est STATIQUE : il lit le
|
||||||
|
depot, zero appel reseau. C'est ce qui le rend rejouable partout, par n'importe qui,
|
||||||
|
et presentable comme piece justificative. Cloner le wiki de la forge pour comparer
|
||||||
|
romprait cette propriete — la seule qui fasse qu'une preuve vaille hors de ce poste.
|
||||||
|
|
||||||
|
`make wiki-publier` depose donc `docs/audit/wiki-publie.yml` : le commit du depot qui
|
||||||
|
a ete publie. Cette preuve compare ce commit a l'etat de `wiki/`.
|
||||||
|
|
||||||
|
CE QU'ELLE NE PROUVE PAS, ET IL FAUT LE SAVOIR AVANT DE S'Y FIER. Elle prouve que le
|
||||||
|
depot n'a pas bouge depuis la derniere publication REUSSIE — pas que la forge porte
|
||||||
|
bien ce contenu. Si quelqu'un edite une page dans l'interface de la forge, le temoin
|
||||||
|
ne le voit pas ; c'est la publication suivante qui l'ecrase, et c'est la regle du
|
||||||
|
depot. Un temoin dit ce qui est PARTI, jamais ce qui est ARRIVE.
|
||||||
|
"""
|
||||||
|
temoin = RACINE / "docs" / "audit" / "wiki-publie.yml"
|
||||||
|
if not temoin.is_file():
|
||||||
|
return False, ("Aucun temoin de publication (docs/audit/wiki-publie.yml) : on ne "
|
||||||
|
"peut pas savoir si le wiki en ligne correspond au depot. "
|
||||||
|
"Publier — `make wiki-publier WIKI_REMOTE=<url>.wiki.git` — depose "
|
||||||
|
"le temoin.")
|
||||||
|
import yaml as _yaml
|
||||||
|
d = _yaml.safe_load(temoin.read_text(encoding="utf-8")) or {}
|
||||||
|
publie = str(d.get("source", "")).strip()
|
||||||
|
if not publie:
|
||||||
|
return False, "docs/audit/wiki-publie.yml sans clef `source` : temoin inexploitable."
|
||||||
|
|
||||||
|
def _git(*a: str) -> str:
|
||||||
|
return subprocess.run(["git", "-C", str(RACINE), *a],
|
||||||
|
capture_output=True, text=True).stdout.strip()
|
||||||
|
|
||||||
|
if _git("cat-file", "-t", publie) != "commit":
|
||||||
|
return False, (f"Le temoin designe le commit `{publie}`, que ce depot ne connait "
|
||||||
|
f"pas — une histoire reecrite, ou un temoin d'un autre depot.")
|
||||||
|
|
||||||
|
# Les commits qui ont touche `wiki/` APRES celui qui a ete publie.
|
||||||
|
depuis = [l for l in _git("log", "--oneline", f"{publie}..HEAD", "--", "wiki/").splitlines() if l]
|
||||||
|
if not depuis:
|
||||||
|
return True, (f"Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis "
|
||||||
|
f"`{publie}` (publie le {d.get('date', '?')}).")
|
||||||
|
dernier = _git("log", "-1", "--format=%h %ad", "--date=short", "--", "wiki/")
|
||||||
|
return False, (f"Le wiki en ligne est EN RETARD : {len(depuis)} commit(s) ont touche "
|
||||||
|
f"`wiki/` depuis la publication de `{publie}` ({d.get('date', '?')}), "
|
||||||
|
f"le dernier etant {dernier}. Ce que la forge sert n'est plus ce que le "
|
||||||
|
f"depot dit. Republier : `make wiki-publier WIKI_REMOTE=<url>.wiki.git`.")
|
||||||
|
|
||||||
|
|
||||||
|
def preuve_enumerations_annoncees() -> tuple[bool, str]:
|
||||||
|
"""Un nombre qui annonce une enumeration correspond a ce qui suit.
|
||||||
|
|
||||||
|
POURQUOI, ALORS QUE P57 GARDE DEJA DES COMPTES (2026-09-07). P57 tient trois comptes
|
||||||
|
NOMMES — preuves, roles, groupes — parce qu'ils se mesurent contre le depot. Les deux
|
||||||
|
ecarts trouves a la main pendant la tournee de documentation etaient d'une autre
|
||||||
|
forme : un nombre qui compte ce que le document lui-meme enumere.
|
||||||
|
|
||||||
|
README.md « Il y en a cinq : » au-dessus d'une table de SIX portes
|
||||||
|
implanter-... « les huit lignes du §7 de … » pour une fiche qui en compte DIX
|
||||||
|
|
||||||
|
CE QUE CETTE PREUVE NE FAIT PAS, ET POURQUOI ELLE SERAIT NUISIBLE AUTREMENT. Une
|
||||||
|
premiere version acceptait toute ligne finissant par « : » et contenant un nombre en
|
||||||
|
toutes lettres. Elle a signale CINQ ecarts sur le corpus, et les cinq etaient du
|
||||||
|
bruit : dans « reprise dans les deux devis : » ou « de quatorze machines : », le
|
||||||
|
nombre qualifie autre chose que l'enumeration qui suit. Cent pour cent de faux
|
||||||
|
positifs — exactement la preuve qui crie sur un cas sain et qu'on apprend a ignorer.
|
||||||
|
|
||||||
|
Elle ne retient donc que les DEUX formes ou le nombre ne peut compter rien d'autre :
|
||||||
|
|
||||||
|
A. « Il y en a <N> : » / « Il y a <N> … : » suivi d'une table ou d'une liste ;
|
||||||
|
B. « les <N> lignes du §<S> de <autre document.md> » — un renvoi chiffre vers la
|
||||||
|
premiere table de cette section-la.
|
||||||
|
|
||||||
|
Une annonce enfouie au milieu d'une phrase reste hors de portee, et c'est assume :
|
||||||
|
mieux vaut une garde etroite qui dit vrai qu'une large qui devine.
|
||||||
|
"""
|
||||||
|
docs = (sorted((RACINE / "docs").glob("*.md")) + sorted((RACINE / "wiki").glob("*.md"))
|
||||||
|
+ [RACINE / "AGENTS.md", RACINE / "README.md", RACINE / "QUICKSTART.md"])
|
||||||
|
mot = "|".join(NOMBRES_FR)
|
||||||
|
forme_a = rf"\bil y (?:en )?a\s+(?:\*\*)?({mot})(?:\*\*)?\b[^:]{{0,20}}:\s*$"
|
||||||
|
forme_b = rf"\bles\s+(?:\*\*)?({mot})(?:\*\*)?\s+lignes\s+du\s+§\s*(\d+)"
|
||||||
|
ecarts: list[str] = []
|
||||||
|
vus = 0
|
||||||
|
|
||||||
|
for d in docs:
|
||||||
|
if not d.is_file() or "/audit/" in str(d):
|
||||||
|
continue
|
||||||
|
texte = d.read_text(encoding="utf-8")
|
||||||
|
lignes = texte.splitlines()
|
||||||
|
|
||||||
|
for i, l in enumerate(lignes):
|
||||||
|
m = re.search(forme_a, l, re.I)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
genre, n = _enumeration_apres(lignes, i)
|
||||||
|
if not genre:
|
||||||
|
continue
|
||||||
|
vus += 1
|
||||||
|
annonce = NOMBRES_FR[m.group(1).lower()]
|
||||||
|
if n != annonce:
|
||||||
|
ecarts.append(f"{d.relative_to(RACINE)}:{i + 1} annonce {annonce}, "
|
||||||
|
f"la {genre} qui suit en compte {n}")
|
||||||
|
|
||||||
|
for m in re.finditer(forme_b, texte, re.I):
|
||||||
|
cible = re.search(r"\(([^)]+\.md)\)", texte[m.end():m.end() + 220])
|
||||||
|
if not cible:
|
||||||
|
continue
|
||||||
|
f = d.parent / cible.group(1)
|
||||||
|
if not f.is_file():
|
||||||
|
continue
|
||||||
|
sec = re.search(rf"^##\s*{m.group(2)}\..*?(?=^##\s|\Z)",
|
||||||
|
f.read_text(encoding="utf-8"), re.M | re.S)
|
||||||
|
if not sec:
|
||||||
|
continue
|
||||||
|
vus += 1
|
||||||
|
annonce = NOMBRES_FR[m.group(1).lower()]
|
||||||
|
n = _lignes_premiere_table(sec.group(0))
|
||||||
|
if n != annonce:
|
||||||
|
ecarts.append(f"{d.relative_to(RACINE)} renvoie aux {annonce} lignes du "
|
||||||
|
f"§{m.group(2)} de {cible.group(1)}, qui en compte {n}")
|
||||||
|
|
||||||
|
if ecarts:
|
||||||
|
return False, ("Des enumerations annoncees ne disent plus vrai :\n - "
|
||||||
|
+ "\n - ".join(ecarts))
|
||||||
|
return True, (f"{vus} enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent "
|
||||||
|
f"(formes non ambigues seulement).")
|
||||||
|
|
||||||
|
|
||||||
|
def preuve_habilitations_declarees() -> tuple[bool, str]:
|
||||||
|
"""Chaque service qui accorde quelque chose declare A QUI, et par quel mecanisme.
|
||||||
|
|
||||||
|
LA LACUNE QUE CETTE PREUVE COMBLE (2026-09-07). `docs/autorisation.md` posait la
|
||||||
|
regle — un service nomme un GROUPE, jamais une personne (D-66) — et `meta/acces.yml`
|
||||||
|
la portait. Rien ne la verifiait : ni qu'un service `web-sso` en porte un, ni que le
|
||||||
|
mecanisme annonce existe. Le §5 d'`authentification.md` dit pourtant l'inverse en
|
||||||
|
toutes lettres : *une directive qu'aucune garde ne verifie finit par ne plus etre
|
||||||
|
vraie*. P29 gardait les POSITIONS d'authentification ; personne ne gardait les
|
||||||
|
HABILITATIONS.
|
||||||
|
|
||||||
|
CE QU'ELLE N'EXIGE PAS, ET C'EST LE POINT LE PLUS IMPORTANT. Elle ne demande PAS que
|
||||||
|
les groupes nommes existent dans l'annuaire. Ce serait contredire la decision
|
||||||
|
centrale du document (§2) : le depot AMORCE un acces et se retire ; les appartenances
|
||||||
|
appartiennent a une personne, et le depot n'a pas a les reconcilier. `amorcage_acces`
|
||||||
|
ne cree qu'un seul groupe ; `dev` et `personnel` sont crees par l'exploitant, et leur
|
||||||
|
absence du code n'est pas un defaut — c'est le regime.
|
||||||
|
|
||||||
|
CE QU'ELLE EXIGE :
|
||||||
|
|
||||||
|
1. tout role `web-sso` porte un `meta/acces.yml` — SAUF `formulaire_local: aucun`,
|
||||||
|
qui designe une passerelle : elle authentifie devant, elle n'accorde rien. Le cas
|
||||||
|
est DERIVE de la declaration, jamais un nom en dur ;
|
||||||
|
2. chaque entree nomme `groupe`, `accorde`, `porte_par` et `raison` ;
|
||||||
|
3. `porte_par` vient de `MECANISMES_ACCES` ;
|
||||||
|
4. `groupe` ressemble a un groupe et non a un compte — ni `@`, ni `uid=`, ni le nom
|
||||||
|
du compte d'amorcage lui-meme ;
|
||||||
|
5. LE CROISEMENT QUI PORTE LA PREUVE : une entree `porte_par: role-realm` dit que
|
||||||
|
l'habilitation voyage par un role de realm PROJETE depuis un groupe LDAP. Ce
|
||||||
|
n'est vrai que si `serveur_keycloak` projette effectivement ce groupe
|
||||||
|
(`acces_projection`). Sans ce controle, un service peut annoncer une habilitation
|
||||||
|
que rien ne transporte, et l'ecran restera vide sans que personne ne sache
|
||||||
|
pourquoi — un chèque vert sur un perimetre vide, exactement.
|
||||||
|
"""
|
||||||
|
import yaml as _yaml
|
||||||
|
roles = RACINE / "roles"
|
||||||
|
fautes: list[str] = []
|
||||||
|
declarees: list[str] = []
|
||||||
|
|
||||||
|
def _acces(nom_role: str) -> dict:
|
||||||
|
f = roles / nom_role / "meta" / "acces.yml"
|
||||||
|
if not f.is_file():
|
||||||
|
return {}
|
||||||
|
return _yaml.safe_load(f.read_text(encoding="utf-8")) or {}
|
||||||
|
|
||||||
|
# Le compte d'amorcage : un `groupe` qui vaudrait ce nom-la designerait une PERSONNE.
|
||||||
|
amorc = _yaml.safe_load(
|
||||||
|
(roles / "amorcage_acces" / "defaults" / "main.yml").read_text(encoding="utf-8")) or {}
|
||||||
|
compte_amorcage = str(amorc.get("amorcage_acces_uid", "")).strip()
|
||||||
|
|
||||||
|
# Ce que Keycloak projette reellement : groupe LDAP -> role(s) de realm.
|
||||||
|
projetes = {str(e.get("groupe", "")).strip()
|
||||||
|
for e in (_acces("serveur_keycloak").get("acces_projection") or [])}
|
||||||
|
|
||||||
|
for d in sorted(x for x in roles.iterdir() if x.is_dir() and x.name.startswith("serveur_")):
|
||||||
|
meta = d / "meta" / "authentification.yml"
|
||||||
|
if not meta.is_file():
|
||||||
|
continue
|
||||||
|
decl = ((_yaml.safe_load(meta.read_text(encoding="utf-8")) or {})
|
||||||
|
.get("authentification") or {})
|
||||||
|
portee = str(decl.get("portee", "")).strip()
|
||||||
|
passerelle = decl.get("formulaire_local") == "aucun"
|
||||||
|
entrees = _acces(d.name).get("acces") or []
|
||||||
|
|
||||||
|
if portee == "web-sso" and not passerelle and not entrees:
|
||||||
|
fautes.append(f"{d.name}: web-sso sans meta/acces.yml — il accorde quelque "
|
||||||
|
f"chose a quelqu'un, et ne dit pas a qui")
|
||||||
|
continue
|
||||||
|
if portee == "web-sso" and passerelle and entrees:
|
||||||
|
fautes.append(f"{d.name}: se declare passerelle (formulaire_local: aucun) mais "
|
||||||
|
f"porte des habilitations — l'un des deux est faux")
|
||||||
|
|
||||||
|
for e in entrees:
|
||||||
|
groupe = str(e.get("groupe", "")).strip()
|
||||||
|
ref = f"{d.name}[{groupe or '?'}]"
|
||||||
|
for champ in ("groupe", "accorde", "porte_par", "raison"):
|
||||||
|
if not str(e.get(champ, "")).strip():
|
||||||
|
fautes.append(f"{ref}: champ `{champ}` vide ou absent")
|
||||||
|
mecanisme = str(e.get("porte_par", "")).strip()
|
||||||
|
if mecanisme and mecanisme not in MECANISMES_ACCES:
|
||||||
|
fautes.append(f"{ref}: mecanisme `{mecanisme}` inconnu "
|
||||||
|
f"(attendus : {', '.join(sorted(MECANISMES_ACCES))})")
|
||||||
|
if groupe and ("@" in groupe or "uid=" in groupe
|
||||||
|
or (compte_amorcage and groupe == compte_amorcage
|
||||||
|
and groupe != str(amorc.get("amorcage_acces_groupe", "")).strip())):
|
||||||
|
fautes.append(f"{ref}: `{groupe}` designe une PERSONNE, pas un groupe (D-66)")
|
||||||
|
if mecanisme == "role-realm" and groupe and groupe not in projetes:
|
||||||
|
fautes.append(f"{ref}: annonce `role-realm`, mais serveur_keycloak ne "
|
||||||
|
f"projette pas le groupe `{groupe}` (acces_projection) — "
|
||||||
|
f"l'habilitation n'est portee par rien")
|
||||||
|
if groupe:
|
||||||
|
declarees.append(ref)
|
||||||
|
|
||||||
|
if fautes:
|
||||||
|
return False, ("Des habilitations ne disent pas vrai :\n - " + "\n - ".join(fautes[:6]))
|
||||||
|
return True, (f"{len(declarees)} habilitation(s) declarees, toutes nommant un groupe, "
|
||||||
|
f"un mecanisme connu et une raison ; les `role-realm` sont projetees.")
|
||||||
|
|
||||||
|
|
||||||
def preuve_authentification() -> tuple[bool, str]:
|
def preuve_authentification() -> tuple[bool, str]:
|
||||||
"""Chaque role serveur_* declare sa position d'authentification, et ne ment pas.
|
"""Chaque role serveur_* declare sa position d'authentification, et ne ment pas.
|
||||||
|
|
@ -2446,6 +2751,12 @@ PREUVES: list[dict] = [
|
||||||
"refs": [], "func": preuve_gabarit_minimal_et_repris},
|
"refs": [], "func": preuve_gabarit_minimal_et_repris},
|
||||||
{"id": "P57", "titre": "Comptes en prose : les chiffres du depot sur lui-meme",
|
{"id": "P57", "titre": "Comptes en prose : les chiffres du depot sur lui-meme",
|
||||||
"refs": [], "func": preuve_comptes_de_la_prose},
|
"refs": [], "func": preuve_comptes_de_la_prose},
|
||||||
|
{"id": "P58", "titre": "Habilitations : chaque service dit a quel GROUPE, et par quoi",
|
||||||
|
"refs": [], "func": preuve_habilitations_declarees},
|
||||||
|
{"id": "P59", "titre": "Enumerations annoncees : le nombre correspond a ce qui suit",
|
||||||
|
"refs": [], "func": preuve_enumerations_annoncees},
|
||||||
|
{"id": "P60", "titre": "Wiki publie : la forge sert ce que le depot dit",
|
||||||
|
"refs": ["AFF-002"], "func": preuve_wiki_publie_a_jour},
|
||||||
{"id": "P43", "titre": "Frontiere : le devis voit les machines du site", "refs": [],
|
{"id": "P43", "titre": "Frontiere : le devis voit les machines du site", "refs": [],
|
||||||
"func": preuve_devis_frontiere_du_site},
|
"func": preuve_devis_frontiere_du_site},
|
||||||
{"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],
|
{"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ Trois idées la portent :
|
||||||
|
|
||||||
- **Le registre** : `docs/audit/affirmations.md` — chaque affirmation du dépôt (README, docs,
|
- **Le registre** : `docs/audit/affirmations.md` — chaque affirmation du dépôt (README, docs,
|
||||||
aide `make`, GUI) reliée à une preuve et un statut (✅/🟡/❌/⚪).
|
aide `make`, GUI) reliée à une preuve et un statut (✅/🟡/❌/⚪).
|
||||||
- **Le harnais** : `make prouver` rejoue les preuves automatisables (**P01–P57**, sans trou dans la série) et écrit
|
- **Le harnais** : `make prouver` rejoue les preuves automatisables (**P01–P60**, sans trou dans la série) et écrit
|
||||||
`docs/audit/preuve-<date>.md`. `make verifier` les inclut : il **échoue** si une preuve échoue.
|
`docs/audit/preuve-<date>.md`. `make verifier` les inclut : il **échoue** si une preuve échoue.
|
||||||
- **Chaque preuve garde une classe d'erreur.** Extrait :
|
- **Chaque preuve garde une classe d'erreur.** Extrait :
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,10 @@ ils descendent tous d'un seul nombre — donc ils ne peuvent pas se contredire.
|
||||||
4. **Change `index` dans un modèle** (jamais en production) et régénère : combien de
|
4. **Change `index` dans un modèle** (jamais en production) et régénère : combien de
|
||||||
valeurs ont bougé ? C'est la mesure exacte de ce que la dérivation t'épargne.
|
valeurs ont bougé ? C'est la mesure exacte de ce que la dérivation t'épargne.
|
||||||
|
|
||||||
**Pour aller plus loin** : [`docs/sdn-evpn.md`](../docs/sdn-evpn.md) (référence
|
**Pour aller plus loin** : `docs/sdn-evpn.md` dans le dépôt (référence technique),
|
||||||
technique), [Le plan & l'adressage dérivé](Le-plan-et-l-adressage-dérivé),
|
[Le plan & l'adressage dérivé](Le-plan-et-l-adressage-dérivé),
|
||||||
[Multi-instance & fédération](Multi-instance-et-fédération).
|
[Multi-instance & fédération](Multi-instance-et-fédération).
|
||||||
|
|
||||||
|
> *Le lien vers `docs/` était relatif (`../docs/…`) : il fonctionne dans le dépôt et
|
||||||
|
> **casse une fois le wiki publié**, la forge ne recevant que le contenu de `wiki/`. C'est
|
||||||
|
> pourquoi le corpus cite `docs/` en **texte**, jamais en lien — une seule page dérogeait.*
|
||||||
|
|
|
||||||
|
|
@ -24,3 +24,14 @@ noms de fichiers `Titre-Avec-Tirets.md` deviennent « Titre Avec Tirets » dans
|
||||||
> `make plan-recette` à partir des sections « ④ à toi de jouer ». La preuve **P22** refuse
|
> `make plan-recette` à partir des sections « ④ à toi de jouer ». La preuve **P22** refuse
|
||||||
> que ce plan soit en retard sur le wiki — modifier une unité sans régénérer fait échouer le
|
> que ce plan soit en retard sur le wiki — modifier une unité sans régénérer fait échouer le
|
||||||
> harnais.
|
> harnais.
|
||||||
|
|
||||||
|
> **Et depuis le 2026-09-07, la preuve P60 refuse que la forge soit en retard sur ce
|
||||||
|
> dossier.** `make wiki-publier` dépose un témoin (`docs/audit/wiki-publie.yml`) : le commit
|
||||||
|
> publié. P60 le compare à l'état de `wiki/`. Rien ne mesurait cet écart auparavant, et il
|
||||||
|
> s'est creusé de **vingt-sept jours** en silence — deux unités jamais publiées, vingt et
|
||||||
|
> une différentes. Pour qui lit la forge plutôt que le dépôt, toute une révision n'existait
|
||||||
|
> pas.
|
||||||
|
>
|
||||||
|
> Ce que le témoin ne dit **pas** : il dit ce qui est *parti*, jamais ce qui est *arrivé*.
|
||||||
|
> Une page éditée dans l'interface de la forge lui échappe — c'est la publication suivante
|
||||||
|
> qui l'écrase, et c'est la règle ci-dessus.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue