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
This commit is contained in:
16 files changed
+2037
-9
No files matched your search
@@ -0,0 +1,75 @@
|
||||
import { prisma } from '@/server/db';
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
/**
|
||||
* Vérifie que la base est configurée pour que la RLS s'applique réellement.
|
||||
*
|
||||
* **Un superutilisateur PostgreSQL contourne toute politique de sécurité au
|
||||
* niveau ligne, y compris avec FORCE.** Connecter l'application avec un tel
|
||||
* compte désactive silencieusement la seconde couche d'isolation : les
|
||||
* requêtes continuent de fonctionner, les tests applicatifs passent, et rien
|
||||
* n'indique que la protection a disparu.
|
||||
*
|
||||
* C'est exactement le genre de mauvaise configuration qu'on ne découvre qu'en
|
||||
* lisant les données d'un autre client. D'où ce contrôle au démarrage.
|
||||
*/
|
||||
|
||||
export interface TenantGuardResult {
|
||||
ok: boolean;
|
||||
isSuperuser: boolean;
|
||||
bypassRls: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export async function checkTenantIsolation(): Promise<TenantGuardResult> {
|
||||
const rows = await prisma.$queryRaw<
|
||||
Array<{ usesuper: boolean; usebypassrls: boolean }>
|
||||
>`SELECT usesuper, usebypassrls FROM pg_user WHERE usename = current_user`;
|
||||
|
||||
const row = rows[0];
|
||||
if (!row) {
|
||||
return {
|
||||
ok: false,
|
||||
isSuperuser: false,
|
||||
bypassRls: false,
|
||||
message: 'Impossible de déterminer les privilèges du compte de connexion.',
|
||||
};
|
||||
}
|
||||
|
||||
const ok = !row.usesuper && !row.usebypassrls;
|
||||
|
||||
return {
|
||||
ok,
|
||||
isSuperuser: row.usesuper,
|
||||
bypassRls: row.usebypassrls,
|
||||
...(ok
|
||||
? {}
|
||||
: {
|
||||
message:
|
||||
'Le compte de connexion contourne la row-level security ' +
|
||||
`(superutilisateur : ${row.usesuper}, bypassrls : ${row.usebypassrls}). ` +
|
||||
"L'isolation multi-tenant ne repose plus que sur la couche applicative. " +
|
||||
'Créer un rôle dédié : CREATE ROLE planflow_app LOGIN NOSUPERUSER NOBYPASSRLS.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Refuse de démarrer en production sur une base mal configurée.
|
||||
*
|
||||
* En développement, un avertissement suffit : la base locale est souvent créée
|
||||
* avec un compte administrateur, et bloquer rendrait la mise en route pénible
|
||||
* sans rien protéger.
|
||||
*/
|
||||
export async function assertTenantIsolation(): Promise<void> {
|
||||
const result = await checkTenantIsolation();
|
||||
if (result.ok) return;
|
||||
|
||||
const message = `Isolation multi-tenant affaiblie — ${result.message}`;
|
||||
|
||||
if (env.NODE_ENV === 'production') {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
console.warn(`⚠ ${message}`);
|
||||
}
|
||||
Reference in new issue
Block a user