Files
planflow/src/instrumentation.ts
T
Claude 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
2026-08-09 08:39:54 +00:00

50 lines
1.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Contrôles au démarrage.
*
* Next.js appelle `register()` une fois, au lancement du serveur.
*/
export async function register(): Promise<void> {
// `instrumentation` est aussi chargée par le runtime edge, où ni Prisma ni
// les sockets PostgreSQL n'existent.
if (process.env.NEXT_RUNTIME !== 'nodejs') return;
const { checkTenantIsolation } = await import('@/server/db-guard');
const { unscoped } = await import('@/server/tenant');
const isolation = await checkTenantIsolation().catch((error: unknown) => {
console.error('Contrôle d’isolation impossible :', error);
return null;
});
if (!isolation || isolation.ok) return;
/*
* Le compte de connexion contourne la row-level security.
*
* Refuser de démarrer se justifie **dès qu'il y a plus d'un compte** : c'est
* alors une fuite entre clients qui devient possible, et aucune donnée ne
* vaut de tourner ainsi. Avec un compte unique — le cas d'une installation
* auto-hébergée ordinaire — il n'y a rien à faire fuir vers un voisin qui
* n'existe pas, et bloquer le démarrage fermerait l'accès de l'entreprise à
* ses propres données pour un risque théorique.
*
* Dans les deux cas l'avertissement est écrit à chaque démarrage, et
* `/api/sante` rapporte `tenantIsolation: weakened`.
*/
const accounts = await unscoped()
.account.count()
.catch(() => 0);
const message = `Isolation multi-tenant affaiblie — ${isolation.message}`;
if (accounts > 1) {
throw new Error(
`${message} La base porte ${accounts} comptes : démarrer ainsi exposerait les données de l'un à l'autre.`,
);
}
console.warn(
`\n⚠ ${message}\n Un seul compte en base : le démarrage se poursuit, mais corrigez la configuration.\n Voir README, « Configuration de la base — à ne pas rater ».\n`,
);
}