Kanban : le 403 du détail d'instance est clos, l'API ouverte reste
Clos : GET /api/Instance/{id} renvoyait 403 à l'app visiteur. Une clé API donne
maintenant accès à sa seule instance, en vue réduite — plan, quotas, TVA,
facturation et pinCode retirés. Déployé en version-3.1.3 et vérifié en préprod
sur cinq cas d'autorisation.
La carte close garde le piège qui vaut pour tout le projet : le test « est-ce un
utilisateur du manager » ne peut pas se baser sur un claim de permission, parce
qu'AuthorizationMiddleware peuple HttpContext.User depuis le schéma ApiKey avant
de court-circuiter sur [AllowAnonymous]. Et elle rectifie une affirmation fausse
de la première rédaction : StripeCustomerId n'est pas exposé par ToDTO.
Nouveau, en Urgent : GET /api/Configuration répond 200 sans aucune clé. Ce n'est
pas une clé mal vérifiée mais un [AllowAnonymous] explicite, probablement hérité
de la v2. Le périmètre exact reste à établir — les autres routes que consomment
les apps visiteur n'ont pas été passées en revue.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
851b062668
commit
b711e654b3
87
kanban.html
87
kanban.html
@ -458,13 +458,13 @@
|
||||
</header>
|
||||
|
||||
<section class="summary" aria-label="Chiffres clés">
|
||||
<div class="stat"><span class="n n-critical">1</span><span class="k">Urgent</span></div>
|
||||
<div class="stat"><span class="n n-critical">2</span><span class="k">Urgent</span></div>
|
||||
<div class="stat"><span class="n n-info">1</span><span class="k">Migration v3</span></div>
|
||||
<div class="stat"><span class="n n-warn">2</span><span class="k">Bugs ouverts</span></div>
|
||||
<div class="stat"><span class="n">10</span><span class="k">À tester</span></div>
|
||||
<div class="stat"><span class="n">11</span><span class="k">À tester</span></div>
|
||||
<div class="stat"><span class="n">36</span><span class="k">Planifié</span></div>
|
||||
<div class="stat"><span class="n n-gate">8</span><span class="k">Bascule prod</span></div>
|
||||
<div class="stat"><span class="n n-good">60</span><span class="k">Fait récemment</span></div>
|
||||
<div class="stat"><span class="n n-gate">9</span><span class="k">Bascule prod</span></div>
|
||||
<div class="stat"><span class="n n-good">62</span><span class="k">Fait récemment</span></div>
|
||||
</section>
|
||||
|
||||
<div class="filters" role="group" aria-label="Filtrer par domaine">
|
||||
@ -490,7 +490,7 @@
|
||||
|
||||
<!-- URGENT -->
|
||||
<section class="col" style="--stripe: var(--critical)">
|
||||
<div class="col-head"><h2>Urgent</h2><span class="count">1</span></div>
|
||||
<div class="col-head"><h2>Urgent</h2><span class="count">2</span></div>
|
||||
<div class="stack">
|
||||
|
||||
<article class="card" data-area="infra">
|
||||
@ -500,6 +500,19 @@
|
||||
<span class="src">todo-features.md — Sécurité réseau</span>
|
||||
</article>
|
||||
|
||||
<article class="card" data-area="backend">
|
||||
<div class="card-meta"><span class="tag">manager-service</span><span class="tag">sécurité</span><span class="flag f-critical">Le contenu de n'importe quelle instance est lisible par qui connaît son identifiant</span></div>
|
||||
<h3>L'API publique répond <strong>sans clé API</strong></h3>
|
||||
<p>Constaté le 08/09 sur la préprod, depuis internet, sans authentification :</p>
|
||||
<pre>curl "https://api.mymuseum.be/api/Configuration?instanceId=633ee379d9405f32f166f047"
|
||||
→ 200, avec le contenu complet et ses traductions</pre>
|
||||
<p>La même requête <strong>avec</strong> <code>X-Api-Key</code> renvoie le même 200. <strong>Cause exacte, vérifiée le 08/09</strong> : <code>Get</code> (<code>ConfigurationController.cs:52</code>) et <code>GetDetailAsync</code> (<code>:117</code>) portent un <code>[AllowAnonymous]</code> <em>explicite</em>. Ce n'est donc pas une clé mal vérifiée, c'est une ouverture assumée — probablement héritée de la v2, où les apps visiteur n'avaient pas de clé. Les apps l'envoient pourtant (<code>client.dart:54</code>), et le mécanisme existe et fonctionne : <code>ApiKeyAuthenticationHandler</code> + la policy <code>AppReadAccess</code>, déjà utilisée par <code>byPin</code> (<code>:83</code>) et <code>export</code> (<code>:383</code>) du même contrôleur.</p>
|
||||
<p><strong>Ce que ça expose</strong> : le contenu éditorial de n'importe quelle instance, à qui connaît un <code>instanceId</code> — lequel se lit en clair dans un APK de flavor, ou se devine à partir d'un export. Pas de données personnelles, mais tout le travail éditorial d'un client.</p>
|
||||
<p>⚠️ <strong>À vérifier avant de conclure</strong> : le périmètre exact. J'ai testé <code>/api/Configuration</code> ; il faut passer en revue les autres routes que consomment les apps visiteur (<code>/api/Section/configuration/{id}</code>, <code>/api/Resource/{id}</code>, <code>/api/SectionMap</code>, <code>/api/SectionQuiz</code>) avant de savoir si le trou est ponctuel ou général.</p>
|
||||
<p>C'est le problème symétrique du 403 sur <code>GET /api/Instance/{id}</code>, <strong>corrigé le 08/09</strong> (voir la carte close) : la même app est <strong>trop bloquée</strong> sur une route et <strong>pas bloquée du tout</strong> sur les autres. Les deux se traitent ensemble, en décidant ce que <code>X-Api-Key</code> est censé garder.</p>
|
||||
<span class="src">conversation 08/09 — vérification de la préprod depuis internet</span>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -547,7 +560,7 @@
|
||||
|
||||
<!-- À TESTER -->
|
||||
<section class="col" style="--stripe: var(--brand)">
|
||||
<div class="col-head"><h2>À tester</h2><span class="count">10</span></div>
|
||||
<div class="col-head"><h2>À tester</h2><span class="count">11</span></div>
|
||||
<div class="stack">
|
||||
|
||||
<article class="card" data-area="visitapp">
|
||||
@ -634,6 +647,19 @@
|
||||
<span class="src">v1-mediatheque-plan.md — les 8 étapes sont codées</span>
|
||||
</article>
|
||||
|
||||
<article class="card" data-area="visitapp">
|
||||
<div class="card-meta"><span class="tag">mymuseum-visitapp</span><span class="tag">UI</span><span class="tag">hors ligne</span><span class="flag f-warn">Jamais vu tourner sur device</span></div>
|
||||
<h3>Home v3 — téléchargement hors ligne porté, et mort de l'ancienne home</h3>
|
||||
<p><strong>Trois trous de <code>home_3.0.dart</code> comblés le 2026-09-08, aucun vu tourner sur device.</strong></p>
|
||||
<p><strong>(1) Les configurations désactivées s'affichaient.</strong> <code>isActive</code> ne vit pas sur <code>ConfigurationDTO</code> mais sur le <em>lien</em> config↔canal (<code>AppConfigurationLinkDTO.isActive</code>, ce que bascule <code>app_configuration_link_screen:407</code>). Ni le fetch par liens ni le set <code>mobileConfigIds</code> ne le lisaient. ⚠️ <strong>L'ancienne home avait le même bug</strong> — ce n'était pas une régression de la v3. La référence était à côté : <code>visitapp-web/src/lib/api/client.ts:170</code> filtre déjà <code>link.isActive && link.configuration</code>.</p>
|
||||
<p><strong>(2) Une visite hors ligne était un cul-de-sac.</strong> Pas de bouton télécharger, pas de garde : la tuile ouvrait <code>ConfigurationPage</code>, et <code>body.dart:250</code> lisant ses sections <em>uniquement</em> en base locale quand <code>isOffline</code> est vrai, le visiteur voyait un détail vide, sans message ni erreur. Porté depuis <code>configurations_list.dart</code> : badge en pastille de verre sur la tuile bento (↓ / ✓), tuile ternie tant qu'elle n'est pas prête, dialogue sombre choix de langue + progression (<code>DownloadConfigurationWidget</code> réutilisé tel quel). Zéro nouvelle clé i18n, tout existait dans les 10 langues.</p>
|
||||
<p>⚠️ <strong>Piège trouvé en câblant</strong> : <code>alreadyDownloaded</code> se déduisait des lignes de la table <code>configurations</code> — table dans laquelle le fetch <strong>écrit une ligne par visite</strong> pour cacher <code>order</code>/<code>gridSpan</code>. Au deuxième lancement, <em>toutes</em> les visites se seraient déclarées téléchargées, et le clic aurait rouvert un détail vide. L'état se déduit maintenant des <strong>sections en base locale</strong>, ce que lit le détail.</p>
|
||||
<p><strong>(3) Passe visuelle.</strong> Nouveau <code>Components/GlassPill.dart</code> (verre dépoli, 3 usages) : le cog devient <code>Icons.tune</code> + drapeau de la langue courante en pastille, <code>GlassesStatusWidget</code> passe sur la même pastille, et la feuille de réglages passe en sombre — une feuille blanche dans une app noire était le contraste le plus violent de l'écran.</p>
|
||||
<p><strong>✅ À supprimer une fois validé sur device : <code>Screens/Home/home.dart</code> et <code>Screens/Home/configurations_list.dart</code>.</strong> Ils ne sont plus référencés par personne — <code>HomePage</code> est orphelin, <code>CustomAppBar</code>, <code>main.dart:220</code>, <code>body.dart:122</code> et <code>menu_page.dart:136</code> pointent tous sur <code>HomePage3</code>. Ils n'étaient gardés que pour le téléchargement, qui vient d'être porté. <strong>Ne les supprimer qu'après avoir joué §21 du test-plan sur device</strong> : ce sont les seules références du comportement d'origine si le portage se révèle incomplet.</p>
|
||||
<p>⚠️ <strong>Deux verrues mises en lumière, pas corrigées.</strong> <code>downloadPrompt</code> annonce « 39,8MB » <strong>en dur dans les 10 langues</strong> et aucune donnée de poids n'existe côté DTO (la colonne <code>weightMasonryGrid</code> a été abandonnée en v3) : soit la taille entre dans l'export, soit le chiffre sort du texte. Et <code>home_3.0.dart:904</code> porte <code>isOnline = true; // Todo remove if not local test</code> — la branche hors ligne <strong>ne s'exécute jamais</strong> (l'analyzer le confirme, <code>dead_code</code> juste après) : une visite téléchargée s'ouvre, mais l'accueil en mode avion passe quand même par le réseau.</p>
|
||||
<span class="src">conversation du 2026-09-08 — captures du Fort de Saint-Héribert</span>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -693,20 +719,25 @@
|
||||
<h3>Carte hors ligne — tile packs Mapbox</h3>
|
||||
<p><strong>Aujourd'hui aucune carte ne fonctionne hors ligne</strong>, et ce n'est pas une question de données : <code>MapDTO</code> porte déjà <code>points</code>, <code>centerLatitude</code>, <code>zoom</code> et <code>iconResourceId</code>, et les icônes sont téléchargées avec la visite. C'est le <strong>fond</strong> qui manque — les tuiles sont chargées en réseau, sans cache.</p>
|
||||
<p><strong>Le GPS, lui, n'est pas le problème</strong> : <code>geolocator ^13.0.0</code> lit le GNSS, qui est autonome. Seule réserve à annoncer au client : sans A-GPS, le premier point peut demander 30 à 60 s sous couvert forestier.</p>
|
||||
<p><strong>Le chemin court existe déjà dans le pubspec</strong> : <code>mapbox_maps_flutter ^2.0.0</code> expose <code>OfflineManager</code> (style packs) et <code>TileStore</code> (tile regions) — on télécharge une emprise bornée au moment du téléchargement de la visite. Pour un domaine comme le Fourneau Saint-Michel, quelques dizaines de Mo. <code>MapProvider.MapBox</code> existe déjà côté modèle.</p>
|
||||
<p><strong>Le chemin court existe déjà dans le pubspec</strong> : la version <strong>résolue est 2.8.0</strong> (pas 2.0.0 comme le laisse croire la contrainte), et le paquet installé contient bien <code>OfflineManager</code>, <code>TileStore</code>, <code>StylePackLoadOptions</code>, <code>TileRegionLoadOptions</code> et <code>TileRegion</code> — <strong>vérifié dans le cache pub, pas supposé</strong>. On télécharge une emprise bornée au moment du téléchargement de la visite ; pour un domaine comme le Fourneau, quelques dizaines de Mo.</p>
|
||||
<p>✅ <strong>Mapbox est désormais le fournisseur par défaut</strong> (04/09) : le <code>null</code> retombait sur Google dans <code>map_page.dart</code> (deux endroits) et <code>map_config.dart</code>. ⚠️ Effet de bord à surveiller — les sections carte existantes de MDLF et Fort Saint-Héribert dont le <code>mapProvider</code> est <code>null</code> <strong>basculent silencieusement de Google à Mapbox</strong>. Le token est déjà en place, en dur dans <code>main.dart:60</code>.</p>
|
||||
<p>⚠️ <strong>Ce n'est pas gratuit, et le compteur est le mauvais</strong> : ce qui coûte n'est pas la surface de l'emprise mais le <strong>nombre de téléchargements</strong> — un tile pack par visiteur. La facture monte donc avec la fréquentation, c'est-à-dire avec le succès du client. Grille à vérifier chez Mapbox, ligne « tile packs » et pas seulement « map loads ». C'est exactement ce calcul que supprime <code>v2/plan-illustre-plan.md</code>.</p>
|
||||
<p>⛔ <strong>Google est une impasse, et pas seulement techniquement.</strong> Le SDK Maps pour Android n'expose aucune API de tuiles hors ligne — les « zones hors connexion » sont une fonction de l'app Google Maps, pas du SDK intégrable. Et le code n'utilise même pas ce SDK pour les sections carte : il passe par <code>flutter_map</code> sur <code>https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}</code>, un endpoint non documenté dont la mise en cache est explicitement interdite. <strong>Conclusion : Mapbox devient le fournisseur du mode hors ligne, Google reste en ligne seulement.</strong></p>
|
||||
<p>⚠️ Tant que ce chantier n'est pas fait, <code>SectionType.Map</code> reste volontairement hors de <code>offlineCapableSectionTypes</code> (<code>downloadConfiguration.dart</code>) : l'ajouter donnerait une carte grise, ce qui est pire que ne pas la proposer. Une fois les tuiles packagées, c'est <strong>une ligne à ajouter dans cette constante</strong>.</p>
|
||||
<span class="src">analyse du code le 2026-09-04 — MapProvider, flutter_map, mapbox_maps_flutter</span>
|
||||
</article>
|
||||
|
||||
<article class="card" data-area="manager backend visitapp" data-horizon="v2">
|
||||
<div class="card-meta"><span class="tag">carto</span><span class="tag">offline</span><span class="tag">manager-app</span><span class="flag f-good">hors ligne par construction, et c'est ce qu'un musée dessine déjà</span></div>
|
||||
<h3>Plan illustré géoréférencé — un 3<sup>e</sup> fournisseur de carte</h3>
|
||||
<p><strong>Un musée de plein air ne distribue pas un fond OpenStreetMap, il distribue son plan dessiné.</strong> Aujourd'hui <code>MapProvider</code> ne connaît que <code>Google</code> et <code>MapBox</code> : il n'existe aucun moyen de téléverser une image de plan et d'y placer les points.</p>
|
||||
<p><strong>Ce qu'il faut</strong> : une troisième valeur de <code>MapProvider</code>, une ressource image portée par la section, et un calage — deux points d'ancrage suffisent (coin haut-gauche / bas-droit en coordonnées réelles) pour projeter un <code>GeoPoint</code> sur l'image et y afficher la position du visiteur.</p>
|
||||
<p><strong>Pourquoi c'est le meilleur des trois</strong> : aucune tuile, donc <strong>hors ligne par construction</strong> — l'image part avec la visite comme n'importe quelle ressource, et le mécanisme de téléchargement n'a rien de nouveau à apprendre. Le rendu est aussi plus lisible qu'un fond routier sur un site de plusieurs dizaines de bâtiments.</p>
|
||||
<p><strong>Périmètre réel</strong> : c'est le plus gros des trois chemins carto — backend (modèle + migration), manager-app (téléversement du plan et pose des ancres), <code>mymuseum-visitapp</code> et <code>visitapp-web</code> (rendu). À faire après <em>Carte hors ligne — tile packs Mapbox</em>, qui débloque le besoin immédiat avec un fond classique.</p>
|
||||
<span class="src">analyse du code le 2026-09-04 — MapProvider n'a que Google et MapBox</span>
|
||||
<div class="card-meta"><span class="tag">carto</span><span class="tag">offline</span><span class="tag">manager-app</span><span class="flag f-good">le seul chemin vers une carte hors ligne sans coût tiers</span></div>
|
||||
<h3>Plan illustré — un 3<sup>e</sup> fournisseur de carte, sans tuiles</h3>
|
||||
<p><strong>Un musée de plein air ne distribue pas un fond OpenStreetMap, il distribue son plan dessiné.</strong> Conception complète, modèle de données et périmètre par repo : <strong><code>DOCS/v2/plan-illustre-plan.md</code></strong>. Rien n'est implémenté.</p>
|
||||
<p><strong>La décision qui structure tout</strong> : les repères se posent <strong>directement sur l'image</strong>, à la main, et c'est cette position qui fait foi — un plan dessiné n'étant ni à l'échelle ni orienté au nord, projeter des <code>GeoPoint</code> depuis leurs coordonnées les ferait tomber à côté des bâtiments. Le calage GPS ne sert qu'à afficher <strong>le visiteur</strong>, et devient donc <strong>facultatif</strong>.</p>
|
||||
<p>⚠️ <strong>Deux affirmations de la première version de cette carte étaient fausses</strong>, corrigées le 04/09 : on ne projette pas les repères, et <strong>deux points d'ancrage ne suffisent pas</strong> — il en faut trois pour une transformation affine qui absorbe rotation et étirement, et pour pouvoir vérifier le calage au lieu de diluer l'erreur.</p>
|
||||
<p>✅ <strong>Le calage se fait assis</strong> : cliquer le même angle de bâtiment sur le plan puis sur une carte réelle en regard. <code>GeolocInputContainer</code> ouvre déjà un <code>FlutterLocationPicker</code> (vraie carte, recherche d'adresse) — le composant est écrit, il faut le mettre à côté du plan. Plus besoin de prestation d'installation.</p>
|
||||
<p>✅ <strong>Hors ligne gratuit</strong> : le plan est une <code>Resource</code>, une ligne dans <code>GetReferencedResourceIds()</code> et le pipeline existant l'embarque. Aucun tiers, aucun quota — contrairement aux tile packs Mapbox, facturés <strong>par visiteur</strong>.</p>
|
||||
<p>⚠️ <strong>Conséquence web à trancher</strong> : <code>visitapp-web</code> reste sur Leaflet (lot E, W1), et <code>mapProviderMobileOnlyNote</code> le dit déjà au client. Pour un plan illustré c'est bloquant — le plan <em>est</em> le produit. Soit on porte le rendu en CSS dans la foulée, soit le <strong>plan Essentiel, web-only, n'y a pas droit</strong>.</p>
|
||||
<p><strong>Livrable qui se vend seul</strong> : le plan <em>sans</em> calage — téléversement, pose des repères, rendu, hors ligne. C'est le produit que les audioguides vendent depuis trente ans.</p>
|
||||
<span class="src">v2/plan-illustre-plan.md — conçu le 2026-09-04</span>
|
||||
</article>
|
||||
|
||||
<article class="card" data-area="backend manager visitapp" data-horizon="v2">
|
||||
@ -1017,7 +1048,7 @@
|
||||
|
||||
<!-- BASCULE PROD -->
|
||||
<section class="col" style="--stripe: var(--gate)">
|
||||
<div class="col-head"><h2>Bascule prod</h2><span class="count">8</span></div>
|
||||
<div class="col-head"><h2>Bascule prod</h2><span class="count">9</span></div>
|
||||
<div class="stack">
|
||||
|
||||
<article class="card" data-area="backend infra">
|
||||
@ -1047,12 +1078,23 @@
|
||||
</article>
|
||||
|
||||
<article class="card" data-area="infra">
|
||||
<div class="card-meta"><span class="tag">étape 18</span><span class="flag f-critical">Filet du jour J</span></div>
|
||||
<div class="card-meta"><span class="tag">étape 18</span><span class="flag f-warn">scripts + destination prêts, attend la prod Postgres</span></div>
|
||||
<h3>pg_dump avant / après + cron quotidien</h3>
|
||||
<p>Noté comme « à faire » depuis juillet et jamais fait. Ici ça cesse d'être une bonne pratique : c'est la seule chose qui permette de recommencer si la bascule tourne mal.</p>
|
||||
<p><strong>Scripts écrits et testés le 07/09</strong> dans <code>manager-service/ManagerService/Deployment/backup/</code> : dump <code>-Fc</code> avec vérification de relecture et rotation, test de restauration comparant les comptages table par table (25 tables identiques), extraction par instance, contrôle de fraîcheur, units systemd, et une procédure de restauration écrite (<code>RESTORE.md</code>).</p>
|
||||
<p><strong>La destination existe depuis le 07/09</strong> : <code>BACKUP_DEST=gcs:unov-myinfomate-backups/pg</code>, service account incapable de supprimer (vérifié par un 403). Voir la carte close « Trancher la destination des sauvegardes hors site ».</p>
|
||||
<p><strong>Ce qui reste</strong> : <code>rclone</code> n'est ni installé ni configuré sur le VPS, et il n'y a de toute façon <strong>pas encore de base Postgres en prod à sauvegarder</strong>. Cette carte est donc une étape à l'intérieur de « Créer l'environnement prod Postgres », pas un chantier parallèle — une vingtaine de minutes le jour venu, plus l'acheminement de la clé du service account en <code>chmod 600</code>.</p>
|
||||
<p>Deux constats du test : les embeddings exclus font passer le dump de 12 Mo à 1,7 Mo, et le « collation version mismatch » du README s'est reproduit — il bloque <code>createdb</code>, donc toute restauration.</p>
|
||||
<span class="src">STATUS.md §4 · §1quinquies — étape 18</span>
|
||||
</article>
|
||||
|
||||
<article class="card" data-area="infra">
|
||||
<div class="card-meta"><span class="tag">étape 18</span><span class="flag f-warn">une alerte que personne ne lit n'est pas une alerte</span></div>
|
||||
<h3>Brancher <code>notify.sh</code> sur un canal réel</h3>
|
||||
<p>Trois surveillances sont en place — échec du script (<code>OnFailure=</code>), sauvegarde plus vieille que 48 h, test de restauration mensuel — et toutes appellent <code>backup/notify.sh</code>, qui ne fait aujourd'hui qu'un <code>logger</code>.</p>
|
||||
<p>Le mode de panne réel n'est pas « le script plante » : c'est « le script ne tourne plus depuis trois semaines et personne ne l'a vu ». C'est précisément celui qu'un <code>OnFailure</code> n'attrape pas et que le contrôle de fraîcheur attrape — à condition que l'alerte sorte de la machine. Renseigner <code>BACKUP_ALERT_WEBHOOK</code>, ou brancher le canal mail du service.</p>
|
||||
<span class="src">conversation 07/09 — chantier sauvegardes</span>
|
||||
</article>
|
||||
|
||||
<article class="card" data-area="backend infra">
|
||||
<div class="card-meta"><span class="tag">étape 19</span></div>
|
||||
<h3>Rejouer pour de vrai, instance par instance</h3>
|
||||
@ -1124,6 +1166,12 @@
|
||||
<span>⛔ <strong>L9 était un malentendu de doc, pas du travail.</strong> La « cible Assistant / Persona » du Mode preview est exactement ce que le §5 du Guide IA a livré le 11/08. Les trois autres cibles (iframes mobile/web/kiosk, viewports, preview-token) sont hors V1. ⚠️ <strong>Mais l'aperçu polluait les stats du client</strong> : chaque essai de personnalité par le gestionnaire était journalisé comme une question de visiteur. Corrigé — et le drapeau d'hier, <code>IsAutoTriggered</code>, est renommé <code>IsVisitorQuestion</code> (défaut <code>true</code>) parce que son vrai sens couvrait déjà les deux cas. <code>dotnet test</code> 218.</span>
|
||||
</div>
|
||||
|
||||
<div class="done-item">
|
||||
<strong>Trancher la destination des sauvegardes hors site</strong>
|
||||
<p>Arrêté et créé le 07/09 : projet GCP <code>myinfomate-backups</code>, bucket <code>gs://unov-myinfomate-backups</code> en <code>EU</code> multi-région, versioning, accès public interdit, lifecycle 400 jours sous <code>pg/</code>. Service account <code>backup-writer</code> en <code>objectCreator</code> + <code>objectViewer</code> — <strong>incapable de supprimer, vérifié par un 403</strong>.</p>
|
||||
<p>Les médias pèsent <strong>1,43 Go</strong> (mesuré, pas estimé) : ils tiennent dans le même bucket sous <code>media/</code>, donc pas d'OVH Object Storage. La disposition croisée envisagée le matin même est abandonnée — elle se justifiait sur un volume supposé de 45 Go, qui venait en réalité du stockage Google One personnel.</p>
|
||||
</div>
|
||||
|
||||
<div class="done-item">
|
||||
<strong>Une conversation, plusieurs surfaces — miroir vocal et <code>conversationId</code></strong>
|
||||
<span>Le chat écrit, le vocal et le proactif partagent désormais <strong>une seule</strong> conversation, portée par <code>VisitAppContext.assistant</code>. Les trois <code>AssistantService</code> séparés ont disparu ; le <code>maxHistory: 6</code> du vocal s'aligne sur 10, deux surfaces qui partagent une conversation ne pouvant pas la tronquer différemment selon le point d'entrée. La concision vocale vient d'<code>isVoice</code>, qui change le prompt côté serveur.</span>
|
||||
@ -1277,6 +1325,11 @@
|
||||
<span>Trouvé en cherchant le « backdoor <code>#if DEBUG</code> » du rapport d'audit : dans <code>AuthenticationController.Authenticate</code>, un bloc <strong>écrasait l'email et le mot de passe reçus</strong> par un compte de test — toute compilation en Debug authentifiait n'importe quelle saisie en tant que <code>test@email.be</code>. Retiré. <code>EnableSensitiveDataLogging</code> (qui écrit les valeurs des paramètres dans les logs : hashes, clés API, données de visiteurs) passe sous <code>#if DEBUG</code>, l'idiome déjà employé dans ce même fichier pour le CORS et Hangfire — et le Dockerfile publiant en <code>-c Release</code>, c'est un vrai verrou, ce qu'un build Release a confirmé. Les autres <code>#if DEBUG</code> du repo sont légitimes.</span>
|
||||
</div>
|
||||
|
||||
<div class="done-item">
|
||||
<strong>L'app visiteur lit le détail d'instance, en vue réduite</strong>
|
||||
<span><strong>Corrigé le 08/09.</strong> <code>GET /api/Instance/{id}</code> renvoyait 403 à <code>mymuseum-visitapp</code>, qui l'appelle au démarrage pour la voix du guide : tout <code>InstanceController</code> porte <code>[Authorize(SuperAdmin)]</code> et <code>GetDetail</code> n'avait pas d'exception, contrairement à <code>slug</code>, <code>byPin</code> et <code>app-key</code>. Une clé API donne désormais accès à <strong>sa seule instance</strong> — clé croisée = 403 — et à une vue réduite : <code>StripCommercialFields</code> retire plan, quotas, usage IA, essai, TVA, facturation et le <code>pinCode</code>, qui ouvre l'appairage des tablettes. Le manager continue de tout voir.<br><br><strong>Le piège, qui vaut pour tout le projet</strong> : le test « est-ce un utilisateur du manager » ne peut PAS se baser sur un claim de permission. <code>AuthorizationMiddleware</code> authentifie avec les schémas de la policy du contrôleur — <code>JwtBearer</code> <em>et</em> <code>ApiKey</code> — et peuple <code>HttpContext.User</code> <strong>avant</strong> de court-circuiter sur <code>[AllowAnonymous]</code>. Une clé API produit donc un <code>User</code> authentifié auquel le handler pose le claim <code>Viewer</code> : la première version laissait passer tout le monde. Elle ne se voyait pas, parce que les champs sensibles de l'instance testée étaient <em>naturellement</em> nuls — c'est le test croisé sur une instance avec un <code>pinCode</code> qui l'a révélée. Le test porte maintenant sur le schéma d'authentification.<br><br>⛔ <strong>Rectification</strong> : la carte annonçait une fuite de <code>StripeCustomerId</code> et <code>StripeSubscriptionId</code>. C'est faux — ils sont sur l'entité mais <strong>pas exposés par <code>ToDTO</code></strong>. Ce qui fuyait vraiment : plan, quotas, TVA, facturation, <code>pinCode</code>.<br><br>Vérifié sur la préprod, cinq cas : 401 sans clé, 200 sur sa propre instance, 403 en croisé dans les deux sens, 200 pour le manager avec le DTO complet. Déployé en <code>version-3.1.3</code>.</span>
|
||||
</div>
|
||||
|
||||
<div class="done-item">
|
||||
<strong>Le client généré nettoyé — et la règle « pas de régénération » enfin verrouillée</strong>
|
||||
<span>Le compte exact : <code>api.dart</code> déclarait <strong>139 <code>part</code> pour 153 fichiers</strong>. Les 14 restants portaient <code>part of openapi.api;</code> sans être listés par leur bibliothèque — en Dart, <strong>hors du graphe de compilation</strong> : l'analyzer les lisait, le compilateur ne les voyait jamais. Toute l'asymétrie qui a coûté du temps était là, et avec elle la garantie que les supprimer ne pouvait pas casser un build. Mesuré : <code>flutter analyze</code> sur le package passe de <strong>67 à 3 issues</strong>, et l'analyse globale de manager-app de <em>68 erreurs de bruit</em> à <strong>3 erreurs</strong>, toutes dans <code>test/widget_test.dart</code> (cassé et connu) — <code>flutter analyze</code> redevient un feu vert utilisable, ce qui débloque les lots design. <code>flutter build web</code> ✅ avant comme après.</span>
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
---
|
||||
title: L'API publique répond <strong>sans clé API</strong>
|
||||
area: backend
|
||||
tags: manager-service, sécurité
|
||||
flag: critical | Le contenu de n'importe quelle instance est lisible par qui connaît son identifiant
|
||||
src: conversation 08/09 — vérification de la préprod depuis internet
|
||||
---
|
||||
<p>Constaté le 08/09 sur la préprod, depuis internet, sans authentification :</p>
|
||||
<pre>curl "https://api.mymuseum.be/api/Configuration?instanceId=633ee379d9405f32f166f047"
|
||||
→ 200, avec le contenu complet et ses traductions</pre>
|
||||
<p>La même requête <strong>avec</strong> <code>X-Api-Key</code> renvoie le même 200. <strong>Cause exacte, vérifiée le 08/09</strong> : <code>Get</code> (<code>ConfigurationController.cs:52</code>) et <code>GetDetailAsync</code> (<code>:117</code>) portent un <code>[AllowAnonymous]</code> <em>explicite</em>. Ce n'est donc pas une clé mal vérifiée, c'est une ouverture assumée — probablement héritée de la v2, où les apps visiteur n'avaient pas de clé. Les apps l'envoient pourtant (<code>client.dart:54</code>), et le mécanisme existe et fonctionne : <code>ApiKeyAuthenticationHandler</code> + la policy <code>AppReadAccess</code>, déjà utilisée par <code>byPin</code> (<code>:83</code>) et <code>export</code> (<code>:383</code>) du même contrôleur.</p>
|
||||
<p><strong>Ce que ça expose</strong> : le contenu éditorial de n'importe quelle instance, à qui connaît un <code>instanceId</code> — lequel se lit en clair dans un APK de flavor, ou se devine à partir d'un export. Pas de données personnelles, mais tout le travail éditorial d'un client.</p>
|
||||
<p>⚠️ <strong>À vérifier avant de conclure</strong> : le périmètre exact. J'ai testé <code>/api/Configuration</code> ; il faut passer en revue les autres routes que consomment les apps visiteur (<code>/api/Section/configuration/{id}</code>, <code>/api/Resource/{id}</code>, <code>/api/SectionMap</code>, <code>/api/SectionQuiz</code>) avant de savoir si le trou est ponctuel ou général.</p>
|
||||
<p>C'est le problème symétrique du 403 sur <code>GET /api/Instance/{id}</code>, <strong>corrigé le 08/09</strong> (voir la carte close) : la même app est <strong>trop bloquée</strong> sur une route et <strong>pas bloquée du tout</strong> sur les autres. Les deux se traitent ensemble, en décidant ce que <code>X-Api-Key</code> est censé garder.</p>
|
||||
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: L'app visiteur lit le détail d'instance, en vue réduite
|
||||
---
|
||||
<span><strong>Corrigé le 08/09.</strong> <code>GET /api/Instance/{id}</code> renvoyait 403 à <code>mymuseum-visitapp</code>, qui l'appelle au démarrage pour la voix du guide : tout <code>InstanceController</code> porte <code>[Authorize(SuperAdmin)]</code> et <code>GetDetail</code> n'avait pas d'exception, contrairement à <code>slug</code>, <code>byPin</code> et <code>app-key</code>. Une clé API donne désormais accès à <strong>sa seule instance</strong> — clé croisée = 403 — et à une vue réduite : <code>StripCommercialFields</code> retire plan, quotas, usage IA, essai, TVA, facturation et le <code>pinCode</code>, qui ouvre l'appairage des tablettes. Le manager continue de tout voir.<br><br><strong>Le piège, qui vaut pour tout le projet</strong> : le test « est-ce un utilisateur du manager » ne peut PAS se baser sur un claim de permission. <code>AuthorizationMiddleware</code> authentifie avec les schémas de la policy du contrôleur — <code>JwtBearer</code> <em>et</em> <code>ApiKey</code> — et peuple <code>HttpContext.User</code> <strong>avant</strong> de court-circuiter sur <code>[AllowAnonymous]</code>. Une clé API produit donc un <code>User</code> authentifié auquel le handler pose le claim <code>Viewer</code> : la première version laissait passer tout le monde. Elle ne se voyait pas, parce que les champs sensibles de l'instance testée étaient <em>naturellement</em> nuls — c'est le test croisé sur une instance avec un <code>pinCode</code> qui l'a révélée. Le test porte maintenant sur le schéma d'authentification.<br><br>⛔ <strong>Rectification</strong> : la carte annonçait une fuite de <code>StripeCustomerId</code> et <code>StripeSubscriptionId</code>. C'est faux — ils sont sur l'entité mais <strong>pas exposés par <code>ToDTO</code></strong>. Ce qui fuyait vraiment : plan, quotas, TVA, facturation, <code>pinCode</code>.<br><br>Vérifié sur la préprod, cinq cas : 401 sans clé, 200 sur sa propre instance, 403 en croisé dans les deux sens, 200 pour le manager avec le DTO complet. Déployé en <code>version-3.1.3</code>.</span>
|
||||
Loading…
x
Reference in New Issue
Block a user