-- WINDOWS #18/#20, 260909-eor Aufgabe 2 — der Anmeldeweg bekommt eine -- bewusst schmale, begruendete Ausnahme von der Mandantentrennung. -- -- DIE ENTSCHEIDUNG UND IHRE BEGRUENDUNG. Von drei erwogenen Wegen faellt die -- Wahl auf SECURITY-DEFINER-Funktionen: -- -- 1. Eine zusaetzliche Policy auf "User" scheidet aus, weil eine Policy -- ein ZEILENPRAEDIKAT ist und nicht die FORM der Abfrage einschraenken -- kann: eine Regel, die eine Suche nach Benutzername erlaubt, erlaubt -- zwangslaeufig auch das Auslesen aller Zeilen. -- 2. Eine zweite Datenbankrolle nur fuer die Anmeldung scheidet aus, weil -- sie einen zweiten Verbindungspool und einen zweiten Prisma-Client -- verlangt und auf "User" ohnehin dieselbe Breite haette. -- 3. Eine Funktion dagegen bindet die Ausnahme an eine FESTE Abfrage mit -- festem Spaltensatz, fester Gleichheitsbedingung und LIMIT 1 — ein -- kompromittierter Aufrufer kann damit einen einzelnen Benutzernamen -- erraten, aber die Tabelle nicht ausleeren. -- -- Alle drei Funktionen sind SECURITY DEFINER, STABLE, mit fest angeheftetem -- Suchpfad auf "public, pg_temp" und LIMIT 1. Der feste Suchpfad ist bei -- SECURITY DEFINER kein Schoenheitsfehler, sondern die eigentliche -- Absicherung: ohne ihn koennte eine untergeschobene Schema-Definition -- (z.B. ein Schema namens "pg_temp" oder ein frueh in search_path -- platziertes Schema mit gleichnamiger Tabelle) den Tabellenbezug in der -- Funktion umlenken, und der Aufrufer erbte die Rechte des Eigentuemers auf -- die falsche Tabelle. -- -- Keine dieser Funktionen schreibt. Nach jeder Anlage werden zuerst -- saemtliche Rechte von PUBLIC entzogen und danach ausschliesslich -- tessera_app das Ausfuehrungsrecht erteilt — die Rolle, die spaeter -- tatsaechlich verbindet (siehe 20260909130000_rls_app_role), ist die -- einzige, die die Ausnahme nutzen darf. -- -- Wiederholbar: CREATE OR REPLACE FUNCTION ist idempotent, REVOKE/GRANT -- sind es ebenfalls. Existiert tessera_app nicht (z.B. auf einer frischen -- Installation vor 20260909130000), scheitert das GRANT laut mit einer -- verstaendlichen Anleitung statt still zu ueberspringen — dasselbe Muster -- wie in 20260909130000_rls_app_role/migration.sql. DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'tessera_app') THEN RAISE EXCEPTION 'tessera_app existiert nicht. Diese Migration setzt 20260909130000_rls_app_role ' 'voraus. Siehe docs/mandantentrennung-datenbankrolle.md.'; END IF; END $$; -- auth_lookup_user_by_username — ersetzt this.prisma.user.findUnique in -- auth.service.ts validateUser() (Zeile 39). Liefert nur die Felder, die -- der Anmeldeweg tatsaechlich braucht: Kennung, Benutzername, Mandant, -- Kennwort-Hash, LDAP-DN, Aktiv-Merkmal, Rolle, Anzeigename, -- Kennwortwechsel-Merkmal. Kein E-Mail-Feld — der Anmeldeweg braucht es -- hier nicht. CREATE OR REPLACE FUNCTION auth_lookup_user_by_username(p_username text) RETURNS TABLE ( id text, username text, "tenantId" text, "passwordHash" text, "ldapDn" text, "isActive" boolean, role "Role", "displayName" text, "mustChangePassword" boolean ) LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public, pg_temp AS $$ SELECT u.id, u.username, u."tenantId", u."passwordHash", u."ldapDn", u."isActive", u.role, u."displayName", u."mustChangePassword" FROM "User" u WHERE u.username = p_username LIMIT 1; $$; REVOKE ALL ON FUNCTION auth_lookup_user_by_username(text) FROM PUBLIC; GRANT EXECUTE ON FUNCTION auth_lookup_user_by_username(text) TO tessera_app; -- auth_lookup_user_by_email — ersetzt this.prisma.user.findUnique in -- auth.service.ts requestPasswordReset() (Zeile 144). Liefert Kennung, -- Mandant, E-Mail und Aktiv-Merkmal; keinen Kennwort-Hash, denn dieser Pfad -- prueft kein Kennwort. CREATE OR REPLACE FUNCTION auth_lookup_user_by_email(p_email text) RETURNS TABLE ( id text, "tenantId" text, email text, "isActive" boolean ) LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public, pg_temp AS $$ SELECT u.id, u."tenantId", u.email, u."isActive" FROM "User" u WHERE u.email = p_email LIMIT 1; $$; REVOKE ALL ON FUNCTION auth_lookup_user_by_email(text) FROM PUBLIC; GRANT EXECUTE ON FUNCTION auth_lookup_user_by_email(text) TO tessera_app; -- auth_lookup_reset_token — ersetzt this.prisma.passwordResetToken.findUnique -- (mit include: { user: true }) in auth.service.ts resetPassword() -- (Zeile 178). Gleichheitsvergleich gegen das Token, liefert den -- Token-Datensatz zusammen mit Benutzerkennung und Mandant des Benutzers. -- Das Token ist eine randomUUID() und nicht erratbar (T-EOR-04). CREATE OR REPLACE FUNCTION auth_lookup_reset_token(p_token text) RETURNS TABLE ( id text, token text, "userId" text, "expiresAt" timestamp(3), "usedAt" timestamp(3), "tenantId" text ) LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public, pg_temp AS $$ SELECT prt.id, prt.token, prt."userId", prt."expiresAt", prt."usedAt", u."tenantId" FROM "PasswordResetToken" prt JOIN "User" u ON u.id = prt."userId" WHERE prt.token = p_token LIMIT 1; $$; REVOKE ALL ON FUNCTION auth_lookup_reset_token(text) FROM PUBLIC; GRANT EXECUTE ON FUNCTION auth_lookup_reset_token(text) TO tessera_app;