Six comments and doc lines put the number of RLS policies at 58. It is
21 — counted from pg_policy while building the data catalogue. The
figure appears in load-bearing prose ("all 58 policies call
is_hr_user()", "all 58 policies stay unchanged"), where being wrong by a
factor of three invites someone to go looking for the missing thirty-
seven.
The two occurrences inside supabase/migrations/ stay as they are. That
file already ran against the database; its comments record what was
believed at the time, and editing them would make the file differ from
what was applied for no gain.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
78 lines
3.1 KiB
TypeScript
78 lines
3.1 KiB
TypeScript
import "server-only";
|
|
import { Kysely, PostgresDialect, sql, type Transaction } from "kysely";
|
|
import { getPool } from "./pool";
|
|
import type { Schema } from "./schema";
|
|
|
|
// Der einzige Weg an die Datenbank.
|
|
//
|
|
// ═══ Warum das keine gewöhnliche Datenbankschicht ist ═══
|
|
//
|
|
// Die Zugriffsrechte liegen in der Datenbank: 21 RLS-Policies rufen
|
|
// is_hr_user() auf, und das fragt seit der Umstellung nicht mehr Supabase,
|
|
// sondern `current_setting('app.user_id')` — eine Sitzungsvariable.
|
|
//
|
|
// Sitzungsvariablen hängen an der *Verbindung*, nicht an der Anfrage. Und
|
|
// Verbindungen kommen aus einem Pool. Wird die Variable ohne Transaktion
|
|
// gesetzt, bleibt sie an der Verbindung kleben, und die nächste Anfrage, die
|
|
// dieselbe Verbindung zieht, läuft mit der Kennung der vorherigen Person —
|
|
// quer über Benutzer hinweg, in einer Personaldatenbank.
|
|
//
|
|
// Das ist die Art Fehler, die in keinem Test auffällt, den man nicht
|
|
// absichtlich dafür schreibt (tests/integration/session-context.test.ts tut
|
|
// genau das). Deshalb:
|
|
//
|
|
// 1. Die Kysely-Instanz wird **nicht exportiert**. Wer abfragen will, muss
|
|
// durch withUser() — und das öffnet immer eine Transaktion.
|
|
// 2. `set_config(..., true)` — das dritte Argument bedeutet
|
|
// transaktionslokal. Mit `false` wäre die ganze Vorsichtsmassnahme
|
|
// wirkungslos.
|
|
// 3. Eine ESLint-Regel verbietet den Import von `pg` und `./pool`
|
|
// ausserhalb dieses Verzeichnisses.
|
|
//
|
|
// Zusätzlich verbindet sich die Anwendung mit einer Datenbankrolle **ohne**
|
|
// BYPASSRLS. Fehlt der Kontext trotz allem, liefern die Policies nichts
|
|
// zurück — nicht alles.
|
|
|
|
// Der Pool wird als Funktion übergeben, nicht als fertige Instanz: Kysely
|
|
// ruft sie erst bei der ersten Abfrage auf. So verlangt der Import dieses
|
|
// Moduls noch keine Zugangsdaten — siehe getPool().
|
|
const db = new Kysely<Schema>({
|
|
dialect: new PostgresDialect({ pool: async () => getPool() }),
|
|
});
|
|
|
|
export type Tx = Transaction<Schema>;
|
|
|
|
/**
|
|
* Führt `fn` im Namen der angegebenen Person aus.
|
|
*
|
|
* `userId` ist die app_users.id. Für nicht angemeldete Zugriffe null — dann
|
|
* greift keine Policy und es kommt nichts zurück, was auch richtig ist.
|
|
*/
|
|
export async function withUser<T>(userId: string | null, fn: (tx: Tx) => Promise<T>): Promise<T> {
|
|
return db.transaction().execute(async (tx) => {
|
|
// Erste Anweisung der Transaktion, vor allem anderen.
|
|
await sql`select set_config('app.user_id', ${userId ?? ""}, true)`.execute(tx);
|
|
return fn(tx);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Für Abläufe ohne angemeldete Person — heute nur der nächtliche Lauf für
|
|
* fällige Änderungen.
|
|
*
|
|
* Bewusst kein privilegierter Zugang: die Verbindung benutzt dieselbe Rolle
|
|
* ohne BYPASSRLS. Was hier laufen darf, muss als SECURITY-DEFINER-Funktion
|
|
* in der Datenbank stehen und dort selbst prüfen, was es tut. Ein
|
|
* Dienstschlüssel, der RLS aushebelt, existiert nicht mehr.
|
|
*/
|
|
export async function asSystem<T>(fn: (tx: Tx) => Promise<T>): Promise<T> {
|
|
return withUser(null, fn);
|
|
}
|
|
|
|
/** Für Migrations- und Wartungsskripte, die ausserhalb einer Anfrage laufen. */
|
|
export async function closeDb(): Promise<void> {
|
|
await db.destroy();
|
|
}
|
|
|
|
export { sql };
|