dd01fb58f4eff1f13a1fbbe3ffbf00a992b5a1e7
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
727c05407a |
Écran de première installation
Les migrations posent le schéma et rien d'autre : une instance neuve n'a aucun compte et aucun utilisateur, donc personne ne peut se connecter. Le seed n'y remédie pas — il installe une démonstration et refuse de tourner en production, à raison. La première visite est désormais redirigée vers `/installation` : nom de l'entreprise, premier établissement avec son fuseau, et compte administrateur. L'écran crée le compte, le catalogue des capacités, les cinq rôles fournis, le propriétaire et son périmètre, puis ouvre la session par le chemin ordinaire — le rôle propriétaire exige aussitôt un second facteur, comme il se doit. Il ne se rouvre pas. Une table `Installation` d'une seule ligne, contrainte en base et protégée par un trigger append-only, marque l'instance. Elle est délibérément hors RLS, et c'est sa raison d'être : la politique d'`Account` ne laisse voir que le compte courant, si bien qu'une instance installée paraîtrait vierge à qui n'a pas de session — et la création d'un propriétaire se rouvrirait à tout venant. Le recensement des politiques porte l'exception, affirmée dans les deux sens. Deux défauts trouvés en éprouvant l'écran sur une base réellement vierge : `INSERT ... RETURNING` sur `Account` était refusé. L'insertion est permise, mais la relecture de la ligne écrite passe par la politique de lecture, qui exige un compte courant. L'identifiant est donc tiré côté application et annoncé avant la création — la règle de partout, appliquée à la transaction qui crée le compte. Et un défaut qui dépassait cet écran : React 19 vide les champs non contrôlés dès qu'une action se termine, refus compris. Les champs vidés portant `required`, le clic suivant était arrêté par la validation du navigateur avant d'émettre un `submit` — le formulaire paraissait mort. Le formulaire de connexion en souffrait aussi ; la suite l'avait manqué parce qu'aucun test ne soumettait deux fois de suite. `PersistentForm` photographie la saisie à l'envoi et la rétablit, mots de passe exclus. Au passage, `Field` rattache son indication par `aria-describedby` : placée dans le `<label>`, elle entrait dans le nom accessible du champ. Éprouvé sur une base vierge, image de production, rôle NOSUPERUSER NOBYPASSRLS : redirection, trois refus motivés, installation, second facteur exigé, écran refermé pour le propriétaire comme pour un visiteur. 450 tests unitaires et d'intégration, 77 tests de bout en bout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
ef2868b980 |
Rendre le déploiement Docker correct, et la RLS réellement active
Trois problèmes, dont un grave, trouvés en corrigeant un échec de déploiement. **Le compose connectait l'application en superutilisateur PostgreSQL.** Un superutilisateur contourne toute politique de sécurité au niveau ligne, y compris déclarée en FORCE : la seconde couche d'isolation était présente en base et absente des faits. Le README l'interdisait déjà noir sur blanc ; le chemin de déploiement que nous livrons faisait exactement l'inverse. Un script d'initialisation crée désormais un rôle `planflow_app` NOSUPERUSER NOBYPASSRLS, propriétaire de la base — il lui faut ce droit pour migrer, et les politiques sont en FORCE précisément pour s'appliquer aussi au propriétaire. Mesuré : en superutilisateur, deux lignes visibles sans compte courant ; avec le rôle dédié, zéro. Basculer la base de développement sur ce même rôle a révélé le défaut que le superutilisateur masquait : `resolveSession` lisait `Membership`, table filtrée par compte, sans périmètre. Avec la RLS active, plus personne ne pouvait se connecter. La résolution passe maintenant par une porte étroite — une politique qui n'ouvre que les lignes dont l'utilisateur est titulaire, sous `app.user_id` — le temps de trouver le compte, puis repasse par le périmètre ordinaire. La suite de tests traverse enfin la RLS au lieu de la contourner. **Les pièces du dossier salarié n'avaient aucun volume.** Elles étaient écrites dans la couche du conteneur et disparaissaient au premier redéploiement. Une pièce d'identité perdue ne se reconstitue pas. Le reste répond à la demande : Postgres préconfiguré — la base n'étant ni publiée ni attachée au réseau du proxy, ce mot de passe protège d'un conteneur voisin, pas d'Internet — réseau `nginx_default` déclaré externe avec l'application seule dessus, et port publié peu courant. ENCRYPTION_KEY reste la seule variable sans valeur par défaut, et n'en aura pas : elle chiffre le NIR, l'IBAN et les arrêts de travail. Une clé livrée avec l'image serait connue de quiconque lit ce dépôt. Au démarrage, l'application contrôle ses propres privilèges : elle refuse de se lancer si la base porte plus d'un compte, et se contente d'un avertissement s'il n'y en a qu'un — bloquer une installation mono-compte fermerait l'accès de l'entreprise à ses données pour une fuite entre clients qui ne peut pas se produire. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
92667b16b9 |
WP-02: locations, teams and the legal configuration register
Adds the referential models, the first database-backed settings screens, and the register the compliance matrix requires before any parameter is enforceable. The register is the point of the lot. The matrix is explicit that copying another product's configuration is not enough — each parameter must carry its value, source, effective date, population and an approver. Approval records the session's actor, never a form field: a signature you can type yourself is worth nothing. The screen names the domains that have no approved parameter yet, so the gap is visible rather than assumed closed. Two bugs of the same family, both now structurally impossible: - The Prisma scoping extension read a hand-written list of models carrying accountId. The four models added here were missing from it, so writes failed with an opaque Prisma error — and a read would have silently returned every account's rows. The list is now derived from the schema itself. - The RLS policies were likewise per-table. A new integration test fails if any table with an accountId column lacks forced RLS and both policies, which is the failure mode that hides best: nobody writes a wrong rule, someone forgets to write one. An end-to-end test signs in as a manager and confirms the settings screens refuse to render — the sidebar hiding them is a convenience, the server check is the control. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
11763142d5 |
WP-01: tenancy, identity and capability authorization
Adds the data model for accounts, locations, teams, users, memberships and scopes, plus roles, the 70-capability catalogue, database-backed sessions, and the audit log. Isolation is enforced twice, independently. A Prisma extension injects accountId into every query, and PostgreSQL row-level security filters underneath it, keyed on a transaction-local setting. The first alone leaves raw queries unguarded; the second alone returns empty results without saying why. Integration tests prove both against a real database rather than through the application layer, which would only prove the application layer. They create a restricted role to do it — and that exposed a trap worth naming: **a PostgreSQL superuser bypasses row-level security even with FORCE**. Connecting the app as one silently disables the second layer while every application test still passes. checkTenantIsolation now refuses to start in production on such a database, warns in development, and reports through /api/sante. The README explains the role to create. The audit log is append-only by trigger, so it resists even a superuser: a trail that can be rewritten proves nothing. Entries carrying an adjustment or an unlock are rejected without a justification, and known secret-bearing fields are redacted before writing — the log is read, exported and kept for years, so it must not become a second unencrypted copy of what is encrypted elsewhere. Sensitive columns use AES-256-GCM with the key held outside the database. Sign-in verifies a dummy hash for unknown accounts so timing does not enumerate addresses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |