Private
Public Access
1
0

Phase 2: Enforce Row-Level Security at the PostgreSQL level

- Rewrite rls.ts with withTenant()/withBypassRLS() using parameterized
  set_config() in interactive Prisma transactions (no SQL injection)
- Add FORCE ROW LEVEL SECURITY on all 15 tenant-scoped tables
- Add __bypass__ sentinel and WITH CHECK clauses to every RLS policy
- Explicitly DISABLE RLS on tenants, tenant_hostnames, sessions
- Wrap all API route and page loader queries in withTenant()
- Wrap validateSession() in withBypassRLS() (joins RLS-protected users)
- Keep existing tenantId where-clause filters as optimization layer
- Add postgresql-client to Dockerfile and auto-apply RLS via entrypoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-25 22:54:26 -05:00
parent f6ddfcfc62
commit bd12160ebb
15 changed files with 625 additions and 424 deletions
+25 -20
View File
@@ -1,5 +1,6 @@
import bcrypt from 'bcrypt';
import { prisma } from './db.js';
import { withBypassRLS } from './rls.js';
const SALT_ROUNDS = 12;
const SESSION_DURATION_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
@@ -21,30 +22,34 @@ export async function createSession(userId: string): Promise<{ id: string; expir
}
export async function validateSession(sessionId: string) {
const session = await prisma.session.findUnique({
where: { id: sessionId },
include: {
user: {
select: {
id: true,
email: true,
name: true,
globalRole: true,
tenantRole: true,
tenantId: true
// Uses withBypassRLS because this joins to the users table (RLS-protected)
// and runs before tenant context is established in hooks.server.ts.
return withBypassRLS(async (tx) => {
const session = await tx.session.findUnique({
where: { id: sessionId },
include: {
user: {
select: {
id: true,
email: true,
name: true,
globalRole: true,
tenantRole: true,
tenantId: true
}
}
}
});
if (!session) return null;
if (session.expiresAt < new Date()) {
await tx.session.delete({ where: { id: sessionId } });
return null;
}
return session;
});
if (!session) return null;
if (session.expiresAt < new Date()) {
await prisma.session.delete({ where: { id: sessionId } });
return null;
}
return session;
}
export async function deleteSession(sessionId: string) {