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
+40 -37
View File
@@ -1,36 +1,37 @@
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types.js';
import { prisma } from '$lib/server/db.js';
import { boardSchema } from '$lib/server/validation.js';
import { withTenant } from '$lib/server/rls.js';
// List boards for current tenant
export const GET: RequestHandler = async ({ locals }) => {
const tenant = locals.tenant;
if (!tenant) throw error(400, 'Unknown tenant');
const where: Record<string, unknown> = { tenantId: tenant.id, archived: false };
const boards = await withTenant(tenant.id, async (tx) => {
const where: Record<string, unknown> = { tenantId: tenant.id, archived: false };
// If not logged in, only show public boards
if (!locals.user) {
where.visibility = 'PUBLIC';
} else {
// Show boards user is a member of + public boards
where.OR = [
{ visibility: 'PUBLIC' },
{ members: { some: { userId: locals.user.id } } }
];
delete where.visibility;
}
// If not logged in, only show public boards
if (!locals.user) {
where.visibility = 'PUBLIC';
} else {
// Show boards user is a member of + public boards
where.OR = [
{ visibility: 'PUBLIC' },
{ members: { some: { userId: locals.user.id } } }
];
}
const boards = await prisma.board.findMany({
where,
include: {
members: {
include: { user: { select: { id: true, name: true, avatarUrl: true } } }
return tx.board.findMany({
where,
include: {
members: {
include: { user: { select: { id: true, name: true, avatarUrl: true } } }
},
_count: { select: { columns: true } }
},
_count: { select: { columns: true } }
},
orderBy: { updatedAt: 'desc' }
orderBy: { updatedAt: 'desc' }
});
});
return json({ boards });
@@ -50,24 +51,26 @@ export const POST: RequestHandler = async ({ request, locals }) => {
const { title, description, visibility } = result.data;
const board = await prisma.board.create({
data: {
tenantId: tenant.id,
title,
description,
visibility,
members: {
create: {
userId: locals.user.id,
role: 'OWNER'
const board = await withTenant(tenant.id, async (tx) => {
return tx.board.create({
data: {
tenantId: tenant.id,
title,
description,
visibility,
members: {
create: {
userId: locals.user!.id,
role: 'OWNER'
}
}
},
include: {
members: {
include: { user: { select: { id: true, name: true, avatarUrl: true } } }
}
}
},
include: {
members: {
include: { user: { select: { id: true, name: true, avatarUrl: true } } }
}
}
});
});
return json({ board }, { status: 201 });