feat(260929-dzu): persoenliche eigene Module je Benutzer (API, Migration, Zeilenschutz)
- ownerUserId (NULL = gemeinsam, sonst persoenlich) mit Zeilenschutz nach Muster SearchProvider - GET nur gemeinsame + eigene, fremde persoenliche Eintraege 404 - POST fuer jeden Benutzer, shared nur fuer Administratoren (403) - PATCH/DELETE: persoenlich nur Besitzer, gemeinsam nur Administrator - Zugriffsklassifikation nachgemessen: 61/223/6 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
-- 260929-dzu — Eigene Module fuer jeden Benutzer: persoenliche Eintraege.
|
||||
--
|
||||
-- Zweck: jeder Benutzer darf eigene Seitenleisten-Eintraege anlegen, die nur
|
||||
-- er selbst sieht. Die Spalte "ownerUserId" unterscheidet: NULL = gemeinsamer
|
||||
-- Eintrag (vom Administrator, fuer alle sichtbar, bisheriges Verhalten),
|
||||
-- gesetzt = persoenlicher Eintrag dieses Benutzers. Faellt der Benutzer weg,
|
||||
-- fallen seine Eintraege mit (ON DELETE CASCADE). Bestehende Zeilen bleiben
|
||||
-- gemeinsam (NULL).
|
||||
--
|
||||
-- Zeilenschutz: Muster "SearchProvider" (20260911120000_rls_user_dimension_
|
||||
-- personal_tables) — Spalte mit NULL = gemeinsame Zeile. Die eine Regel
|
||||
-- "tenant_isolation_policy" (aus 20260929120000, ohne Benutzerdimension) wird
|
||||
-- durch vier nach Befehl getrennte Regeln ersetzt (Praezedenz 260910-jab (3)):
|
||||
-- ein einzelner USING-Ausdruck, der die gemeinsame Zeile zum Lesen einschliesst,
|
||||
-- wuerde sie sonst auch zum Aendern/Entfernen freigeben.
|
||||
-- SELECT: Mandant UND (kein Benutzer gesetzt ODER gemeinsame Zeile ODER
|
||||
-- eigene Zeile).
|
||||
-- INSERT/UPDATE/DELETE: Mandant UND (kein Benutzer gesetzt ODER eigene
|
||||
-- Zeile). Ein Benutzerkontext kann gemeinsame Zeilen also NICHT
|
||||
-- schreiben; der Administrator-Weg fuer gemeinsame Eintraege bindet
|
||||
-- deshalb ohne Benutzer (`forTenant(prisma, tenantId)`), die
|
||||
-- Rollenpruefung liegt im Controller/Dienst.
|
||||
-- Die Regelnamen sind neu (vier statt eine), rls-coverage.spec.ts fordert nur
|
||||
-- mindestens eine Regel je Tabelle mit eingeschaltetem RLS.
|
||||
--
|
||||
-- Rechte fuer tessera_app kommen ueber ALTER DEFAULT PRIVILEGES aus
|
||||
-- 20260909130000_rls_app_role — hier nichts zu tun.
|
||||
--
|
||||
-- WICHTIG: wie alle RLS-Regeln dieses Schemas wirken diese erst, wenn die
|
||||
-- Anwendung als Rolle ohne Umgehungsrecht verbindet (Schalter heute AUS, siehe
|
||||
-- docs/mandantentrennung-datenbankrolle.md). Bis dahin tragen die
|
||||
-- Anwendungspruefungen im Dienst den Schutz allein.
|
||||
|
||||
ALTER TABLE "CustomModule" ADD COLUMN "ownerUserId" TEXT;
|
||||
|
||||
CREATE INDEX "CustomModule_tenantId_ownerUserId_idx" ON "CustomModule"("tenantId", "ownerUserId");
|
||||
|
||||
ALTER TABLE "CustomModule" ADD CONSTRAINT "CustomModule_ownerUserId_fkey"
|
||||
FOREIGN KEY ("ownerUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
DROP POLICY tenant_isolation_policy ON "CustomModule";
|
||||
|
||||
CREATE POLICY tenant_user_read_policy ON "CustomModule"
|
||||
FOR SELECT
|
||||
USING (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (
|
||||
current_user_id() IS NULL
|
||||
OR "ownerUserId" IS NULL
|
||||
OR "ownerUserId" = current_user_id()
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY tenant_user_insert_policy ON "CustomModule"
|
||||
FOR INSERT
|
||||
WITH CHECK (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "ownerUserId" = current_user_id())
|
||||
);
|
||||
|
||||
CREATE POLICY tenant_user_update_policy ON "CustomModule"
|
||||
FOR UPDATE
|
||||
USING (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "ownerUserId" = current_user_id())
|
||||
)
|
||||
WITH CHECK (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "ownerUserId" = current_user_id())
|
||||
);
|
||||
|
||||
CREATE POLICY tenant_user_delete_policy ON "CustomModule"
|
||||
FOR DELETE
|
||||
USING (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "ownerUserId" = current_user_id())
|
||||
);
|
||||
@@ -53,6 +53,7 @@ model User {
|
||||
passwordResetTokens PasswordResetToken[]
|
||||
groupMemberships GroupMembership[]
|
||||
moduleGrants ModuleGrant[]
|
||||
customModules CustomModule[]
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([username])
|
||||
@@ -727,8 +728,14 @@ model CustomModule {
|
||||
name String
|
||||
url String
|
||||
category String // eine der MODULE_CATEGORIES aus @tessera/shared
|
||||
// quick-260929-dzu: null = gemeinsamer Eintrag (vom Administrator, fuer alle
|
||||
// sichtbar); gesetzt = persoenlicher Eintrag, nur fuer diesen Benutzer
|
||||
// sichtbar. Faellt der Benutzer weg, fallen seine Eintraege mit.
|
||||
ownerUserId String?
|
||||
owner User? @relation(fields: [ownerUserId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([tenantId, ownerUserId])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user