Projekt-Fundament: Spec, Docker-Stack, Prisma-Basis, Tenant-Guard
- docs/SPEC.md (v1.2: Assets & BIA als ein Modul, Risiko-Detailansicht mit Maßnahmen/betroffenen Assets, Risiko-Verknüpfungen in allen Detailansichten) + UI-Mockup als Referenz - Docker-Compose: app, worker, postgres (pgvector), redis, minio, mailhog - Prisma 7: Fundament-Schema (Tenant, User, RBAC, AuditLog), prisma.config.ts, pg-Treiberadapter - Tenant-Guard (src/server/db.ts): erzwungene Mandanten-Isolation für alle tenant-gebundenen Modelle - AGENTS.md/README mit Projektregeln (Isolation, RBAC, Audit-Log, i18n, Lizenz) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
|
||||
/**
|
||||
* Central database access for the ISMS tool.
|
||||
*
|
||||
* Multi-tenant rule (see docs/SPEC.md §2): no query on tenant-scoped models
|
||||
* without a tenant context. Application code MUST use `dbForTenant(tenantId)`
|
||||
* for all tenant-scoped models — the raw client is only for global data
|
||||
* (permissions, control catalogs) and platform administration.
|
||||
*
|
||||
* Postgres Row Level Security is added on top as a second line of defense
|
||||
* (prisma/rls.sql, applied via migration).
|
||||
*/
|
||||
|
||||
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
|
||||
|
||||
/** Models that carry a tenantId column and must never be queried without one. */
|
||||
const TENANT_MODELS = new Set<string>(["User", "Role", "AuditLog"]);
|
||||
|
||||
/**
|
||||
* Returns a Prisma client that transparently enforces the tenant scope:
|
||||
* reads are filtered by tenantId, creates get the tenantId injected.
|
||||
*/
|
||||
export function dbForTenant(tenantId: string) {
|
||||
if (!tenantId) throw new Error("dbForTenant: tenantId is required");
|
||||
|
||||
return prisma.$extends({
|
||||
query: {
|
||||
$allModels: {
|
||||
async $allOperations({ model, operation, args, query }) {
|
||||
if (!TENANT_MODELS.has(model)) return query(args);
|
||||
|
||||
const a = args as Record<string, unknown>;
|
||||
|
||||
if (
|
||||
operation === "findMany" ||
|
||||
operation === "findFirst" ||
|
||||
operation === "count" ||
|
||||
operation === "aggregate" ||
|
||||
operation === "updateMany" ||
|
||||
operation === "deleteMany"
|
||||
) {
|
||||
a.where = { AND: [{ tenantId }, (a.where as object) ?? {}] };
|
||||
} else if (operation === "create") {
|
||||
a.data = { ...(a.data as object), tenantId };
|
||||
} else if (operation === "createMany") {
|
||||
const data = a.data as Record<string, unknown>[];
|
||||
a.data = data.map((d) => ({ ...d, tenantId }));
|
||||
} else if (operation === "findUnique" || operation === "findUniqueOrThrow") {
|
||||
// Unique lookups cannot be widened with AND — verify ownership on the result.
|
||||
const result = await query(args);
|
||||
if (
|
||||
result &&
|
||||
typeof result === "object" &&
|
||||
"tenantId" in result &&
|
||||
(result as { tenantId: string }).tenantId !== tenantId
|
||||
) {
|
||||
throw new Error(
|
||||
`Tenant isolation violation: ${model} belongs to another tenant`
|
||||
);
|
||||
}
|
||||
return result;
|
||||
} else if (
|
||||
operation === "update" ||
|
||||
operation === "delete" ||
|
||||
operation === "upsert"
|
||||
) {
|
||||
// Mutations on unique keys: verify ownership BEFORE mutating.
|
||||
const delegate = (
|
||||
prisma as unknown as Record<
|
||||
string,
|
||||
{ findUnique: (a: unknown) => Promise<{ tenantId?: string } | null> }
|
||||
>
|
||||
)[model.charAt(0).toLowerCase() + model.slice(1)];
|
||||
const existing = await delegate.findUnique({ where: a.where });
|
||||
if (existing && existing.tenantId !== tenantId) {
|
||||
throw new Error(
|
||||
`Tenant isolation violation: ${model} belongs to another tenant`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return query(args);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export type TenantDb = ReturnType<typeof dbForTenant>;
|
||||
Reference in New Issue
Block a user