feat(quick-260910-jab): drei zu kurz greifende RLS-Regeln schliessen (T-JTS-02, T-JTS-03, WINDOWS #19)
- Neue, handgeschriebene Migration 20260910120000_rls_widen_membership_grant_and_platform_read: GroupMembership prueft jetzt beide Seiten der Beziehung (Gruppe UND Benutzer), ModuleGrant prueft zusaetzlich die referenzierte Gruppe/den referenzierten Benutzer (mit Leer-Zulassung, D-04), TenderRssFeedSource bekommt vier nach Befehl getrennte Regeln statt einer (Lesen schliesst plattformweite Zeilen ein, Schreiben verlangt weiterhin einen Mandanten). SearchProvider bewusst unveraendert (Befund E: Praemisse widerlegt). Lokal angewandt und gegen den Systemkatalog der lebenden Datenbank gemessen. Der Schalter bleibt aus (Rolle tessera). - rls-scratch-check.mjs: die drei loch-behauptenden Pruefungen umgekehrt (nicht geloescht), Gegenmessungen ueber die Wartungsrolle ergaenzt, vier Befehlsrichtungen fuer TenderRssFeedSource gemessen, neuer Abschnitt fuer SearchProvider, Extraktion auf die neue Migration umgeleitet und um eine mehrfach-treffer-faehige Form ergaenzt (extractAllPolicySql). - migration-sql.spec.ts: neuer Beschreibungsblock fuer die neue Migration. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
+124
@@ -0,0 +1,124 @@
|
|||||||
|
-- 260910-jab, Aufgabe 1 — schliesst T-JTS-02, T-JTS-03 und WINDOWS #19: drei
|
||||||
|
-- Regeln, die kuerzer greifen als sie sollen.
|
||||||
|
--
|
||||||
|
-- Loest drei ausgelieferte Regeln ab. Die betroffenen Dateien
|
||||||
|
-- (20260804130130_add_groups_and_module_grants fuer die Tabellenform,
|
||||||
|
-- 20260804130918_groups_rls_policies fuer die alte GroupMembership/
|
||||||
|
-- ModuleGrant-Regel, 20260909140000_rls_remaining_tenant_tables fuer die
|
||||||
|
-- alte TenderRssFeedSource-Regel) bleiben UNVERAENDERT stehen — Prisma
|
||||||
|
-- fuehrt ihre Pruefsumme, eine Aenderung braechte "prisma migrate deploy"
|
||||||
|
-- zum Abbruch. Praezedenzfall und Kopfform: 20260909140000_rls_remaining_
|
||||||
|
-- tenant_tables.
|
||||||
|
--
|
||||||
|
-- (1) GroupMembership (T-JTS-02): die ausgelieferte Regel prueft
|
||||||
|
-- ausschliesslich, ob die referenzierte Gruppe zum laufenden Mandanten
|
||||||
|
-- gehoert ("groupId" IN (...)) — nicht, ob der referenzierte Benutzer es
|
||||||
|
-- tut. Eine Mitgliedschaft konnte dadurch eine Gruppe des einen Mandanten
|
||||||
|
-- mit einem Benutzer eines anderen verbinden. Die neue Regel prueft beide
|
||||||
|
-- Seiten mit UND verknuepft, nach demselben Join-Muster, das
|
||||||
|
-- PasswordResetToken seit 20260618112133 fuer die Benutzerseite vormacht.
|
||||||
|
--
|
||||||
|
-- (2) ModuleGrant (T-JTS-03): die ausgelieferte Regel prueft ausschliesslich
|
||||||
|
-- die Mandantenkennung der Zeile selbst — nicht, wohin "groupId"/"userId"
|
||||||
|
-- zeigen. Eine Freigabe mit korrekter eigener Mandantenkennung, aber
|
||||||
|
-- fremder Gruppen- ODER fremder Benutzerkennung, wurde durchgelassen. Die
|
||||||
|
-- neue Regel prueft zusaetzlich beide moeglichen Ziele; die Leer-Zulassung
|
||||||
|
-- ist zwingend, weil das Modell Gruppe und Benutzer als Entweder-oder fuehrt
|
||||||
|
-- (D-04, CHECK-Constraint "ModuleGrant_group_xor_user"). WICHTIG:
|
||||||
|
-- `assertTargetBelongsToTenant` in apps/api/src/groups/module-grants.service.ts
|
||||||
|
-- bleibt UNVERAENDERT bestehen — diese Datenbankregel ist ein ZWEITES Netz,
|
||||||
|
-- kein Ersatz dafuer.
|
||||||
|
--
|
||||||
|
-- (3) TenderRssFeedSource (WINDOWS #19): "tenantId" ist nullable — NULL
|
||||||
|
-- markiert eine plattformweite Zeile (D-06). Die ausgelieferte Regel
|
||||||
|
-- "tenantId" = current_tenant_id() vergleicht NULL nie gleich; eine
|
||||||
|
-- plattformweite Zeile waere nach dem Scharfschalten fuer JEDEN Mandanten
|
||||||
|
-- unsichtbar, nicht nur fuer fremde. Ersetzt durch VIER nach Befehl
|
||||||
|
-- getrennte Regeln: die Leseregel schliesst die plattformweiten Zeilen
|
||||||
|
-- ausdruecklich ein, die drei Schreibregeln (Einfuegen/Aendern/Entfernen)
|
||||||
|
-- verlangen weiterhin ausnahmslos einen Mandanten. Vier ausdrueckliche
|
||||||
|
-- Regeln statt einer mit stillschweigender Wirkung, weil ein einzelner
|
||||||
|
-- USING-Ausdruck auch bestimmt, welche Zeilen UPDATE und DELETE ueberhaupt
|
||||||
|
-- erreichen — eine Regel, die die plattformweiten Zeilen zum Lesen
|
||||||
|
-- einschliesst, wuerde ohne die Trennung jedem Mandanten auch das Aendern
|
||||||
|
-- und Entfernen dieser Zeilen erlauben.
|
||||||
|
--
|
||||||
|
-- (4) SearchProvider — bewusst UNVERAENDERT, keine Anweisung in dieser
|
||||||
|
-- Migration. "tenantId" ist hier ebenfalls nullable, aber die Praemisse von
|
||||||
|
-- WINDOWS #19 stimmt fuer diese Tabelle nachweislich NICHT: es gibt keinen
|
||||||
|
-- Codeweg, der eine mandantenlose Zeile erzeugt — der einzige Schreibweg
|
||||||
|
-- (apps/api/src/dashboard/dashboard.service.ts) verlangt die
|
||||||
|
-- Mandantenkennung als Pflichtparameter, und die Vorgabe-Suchmaschinen sind
|
||||||
|
-- Konstanten (Entscheidung 05-02), keine Datenbankzeilen. Lokal gemessen
|
||||||
|
-- (2026-09-10): null Zeilen insgesamt in "SearchProvider". Eine Lockerung
|
||||||
|
-- waere hier die falsche Richtung — sie wuerde eine kuenftige mandantenlose
|
||||||
|
-- Zeile jedem Mandanten zeigen. Die Schliessung von WINDOWS #19 schliesst
|
||||||
|
-- diese Haelfte deshalb als WIDERLEGTE PRAEMISSE, nicht als geloestes
|
||||||
|
-- Problem.
|
||||||
|
--
|
||||||
|
-- WICHTIG: wie alle bisherigen RLS-Migrationen wirken diese Regeln erst,
|
||||||
|
-- wenn die Anwendung als Rolle ohne Umgehungsrecht verbindet (siehe
|
||||||
|
-- 20260909130000_rls_app_role und docs/mandantentrennung-datenbankrolle.md).
|
||||||
|
-- Die Verbindung ist zum Zeitpunkt dieser Migration weiterhin NICHT
|
||||||
|
-- umgestellt — `DATABASE_URL` zeigt unveraendert auf die Rolle `tessera`.
|
||||||
|
-- Ohne diesen Satz waere diese Datei genau das, wovor WINDOWS #18 warnt:
|
||||||
|
-- eine Regel, die Sicherheit vortaeuscht.
|
||||||
|
|
||||||
|
-- (1) GroupMembership — beide Seiten der Beziehung.
|
||||||
|
DROP POLICY tenant_isolation_policy ON "GroupMembership";
|
||||||
|
|
||||||
|
CREATE POLICY tenant_isolation_policy ON "GroupMembership"
|
||||||
|
USING (
|
||||||
|
"groupId" IN (
|
||||||
|
SELECT "id" FROM "Group" WHERE "tenantId" = current_tenant_id()
|
||||||
|
)
|
||||||
|
AND "userId" IN (
|
||||||
|
SELECT "id" FROM "User" WHERE "tenantId" = current_tenant_id()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- (2) ModuleGrant — die Zeile selbst UND beide moeglichen Ziele.
|
||||||
|
DROP POLICY tenant_isolation_policy ON "ModuleGrant";
|
||||||
|
|
||||||
|
CREATE POLICY tenant_isolation_policy ON "ModuleGrant"
|
||||||
|
USING (
|
||||||
|
"tenantId" = current_tenant_id()
|
||||||
|
AND (
|
||||||
|
"groupId" IS NULL
|
||||||
|
OR "groupId" IN (
|
||||||
|
SELECT "id" FROM "Group" WHERE "tenantId" = current_tenant_id()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
"userId" IS NULL
|
||||||
|
OR "userId" IN (
|
||||||
|
SELECT "id" FROM "User" WHERE "tenantId" = current_tenant_id()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- (3) TenderRssFeedSource — Lesen schliesst die plattformweiten Zeilen ein,
|
||||||
|
-- Schreiben (Einfuegen/Aendern/Entfernen) verlangt ausnahmslos einen
|
||||||
|
-- Mandanten. Vier Regeln statt einer, nach Befehl getrennt (Begruendung
|
||||||
|
-- oben).
|
||||||
|
DROP POLICY tenant_isolation_policy ON "TenderRssFeedSource";
|
||||||
|
|
||||||
|
CREATE POLICY tenant_platform_read_policy ON "TenderRssFeedSource"
|
||||||
|
FOR SELECT
|
||||||
|
USING ("tenantId" = current_tenant_id() OR "tenantId" IS NULL);
|
||||||
|
|
||||||
|
CREATE POLICY tenant_insert_policy ON "TenderRssFeedSource"
|
||||||
|
FOR INSERT
|
||||||
|
WITH CHECK ("tenantId" = current_tenant_id());
|
||||||
|
|
||||||
|
CREATE POLICY tenant_update_policy ON "TenderRssFeedSource"
|
||||||
|
FOR UPDATE
|
||||||
|
USING ("tenantId" = current_tenant_id())
|
||||||
|
WITH CHECK ("tenantId" = current_tenant_id());
|
||||||
|
|
||||||
|
CREATE POLICY tenant_delete_policy ON "TenderRssFeedSource"
|
||||||
|
FOR DELETE
|
||||||
|
USING ("tenantId" = current_tenant_id());
|
||||||
|
|
||||||
|
-- (4) SearchProvider — keine Anweisung. Die ausgelieferte Regel
|
||||||
|
-- ("tenantId" = current_tenant_id()) bleibt unveraendert bestehen.
|
||||||
@@ -389,6 +389,40 @@ function extractPolicySql(migrationSql, tableName) {
|
|||||||
return match ? match[0] : null;
|
return match ? match[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest die Migration, die T-JTS-02, T-JTS-03 und WINDOWS #19 schliesst
|
||||||
|
* (Dateiname endet auf "_rls_widen_membership_grant_and_platform_read",
|
||||||
|
* 260910-jab). Die beiden abgeloesten Regeln (GroupMembership, ModuleGrant)
|
||||||
|
* MUESSEN ab hier aus dieser Datei extrahiert werden, nicht mehr aus
|
||||||
|
* `readGroupsRlsPoliciesMigrationSql()` — sonst misst dieses Werkzeug
|
||||||
|
* weiter die abgeloeste Regel (Befund D).
|
||||||
|
*/
|
||||||
|
function readRlsWidenMigrationSql() {
|
||||||
|
const dirs = readdirSync(MIGRATIONS_DIR, { withFileTypes: true })
|
||||||
|
.filter(
|
||||||
|
(entry) =>
|
||||||
|
entry.isDirectory() &&
|
||||||
|
entry.name.endsWith('_rls_widen_membership_grant_and_platform_read'),
|
||||||
|
)
|
||||||
|
.map((entry) => entry.name);
|
||||||
|
if (dirs.length !== 1) return null;
|
||||||
|
return readFileSync(join(MIGRATIONS_DIR, dirs[0], 'migration.sql'), 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wie extractPolicySql(), aber liefert ALLE `CREATE POLICY ... ON
|
||||||
|
* "<Tabelle>"`-Anweisungen einer Tabelle statt nur der ersten (Befund D:
|
||||||
|
* extractPolicySql() hat keinen globalen Flag und kennt nur EINEN
|
||||||
|
* Policy-Namen — fuer eine Tabelle mit mehreren, nach Befehl getrennten
|
||||||
|
* Regeln unter verschiedenen Namen reicht das nicht). Policy-Name ist
|
||||||
|
* absichtlich ein Platzhalter (`\w+`), nicht `tenant_isolation_policy` —
|
||||||
|
* die vier Regeln auf "TenderRssFeedSource" heissen unterschiedlich.
|
||||||
|
*/
|
||||||
|
function extractAllPolicySql(migrationSql, tableName) {
|
||||||
|
const re = new RegExp(`CREATE POLICY \\w+ ON "${tableName}"[\\s\\S]*?;`, 'g');
|
||||||
|
return [...migrationSql.matchAll(re)].map((m) => m[0]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aufgabe 1 (260909-ipc) — misst die fuenf im Plan genannten Verhaltensweisen
|
* Aufgabe 1 (260909-ipc) — misst die fuenf im Plan genannten Verhaltensweisen
|
||||||
* des Bereichs ldap unter der Rolle ohne BYPASSRLS, mit den beiden Policies
|
* des Bereichs ldap unter der Rolle ohne BYPASSRLS, mit den beiden Policies
|
||||||
@@ -565,28 +599,37 @@ function readRemainingTenantTablesMigrationSql() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aufgabe 1 (260909-jts), TEIL 1 — misst die im Plan genannten
|
* Aufgabe 1 (260909-jts/260910-jab), TEIL 1 — misst die im Plan genannten
|
||||||
* Verhaltensweisen des Bereichs groups unter der Rolle ohne BYPASSRLS, mit
|
* Verhaltensweisen des Bereichs groups unter der Rolle ohne BYPASSRLS, mit
|
||||||
* den vier Policies WORTGLEICH aus den beiden ausgelieferten Migrationen
|
* den vier Policies WORTGLEICH aus den ausgelieferten Migrationen (nicht im
|
||||||
* (nicht im Werkzeug nachgetippt, vgl. runLdapAreaChecks). Findet die
|
* Werkzeug nachgetippt, vgl. runLdapAreaChecks). Findet die Extraktion eine
|
||||||
* Extraktion eine der vier nicht, meldet dieser Abschnitt eine
|
* der vier nicht, meldet dieser Abschnitt eine FEHLGESCHLAGENE Pruefung und
|
||||||
* FEHLGESCHLAGENE Pruefung und bricht ab, statt mit einer geratenen Policy
|
* bricht ab, statt mit einer geratenen Policy weiterzumessen.
|
||||||
* weiterzumessen.
|
*
|
||||||
|
* GroupMembership und ModuleGrant werden seit 260910-jab (T-JTS-02,
|
||||||
|
* T-JTS-03) aus `readRlsWidenMigrationSql()` gelesen, NICHT mehr aus
|
||||||
|
* `readGroupsRlsPoliciesMigrationSql()` — jene Datei traegt noch die
|
||||||
|
* abgeloeste, kuerzer greifende Fassung (Befund D). "Group" bleibt
|
||||||
|
* unveraendert und wird weiterhin aus der urspruenglichen Datei gelesen.
|
||||||
*
|
*
|
||||||
* Legt die Tabelle "Group" (samt je einer Zeile fuer TENANT-A und
|
* Legt die Tabelle "Group" (samt je einer Zeile fuer TENANT-A und
|
||||||
* TENANT-B) an, auf der runTransactionShapeMeasurement() weiter unten
|
* TENANT-B) an, auf der runTransactionShapeMeasurement() weiter unten
|
||||||
* aufsetzt — diese Funktion muss deshalb VOR jener aufgerufen werden.
|
* aufsetzt — diese Funktion muss deshalb VOR jener aufgerufen werden. Setzt
|
||||||
|
* ausserdem auf der Tabelle "User" auf, die runAuthLookupChecks() vorher
|
||||||
|
* bereits angelegt hat (Zeilen user-a/TENANT-A, user-b/TENANT-B) — die neue
|
||||||
|
* GroupMembership/ModuleGrant-Regel prueft ueber diese Tabelle.
|
||||||
*/
|
*/
|
||||||
async function runGroupsAreaChecks(adminUrl, scratchRoleUrl, results) {
|
async function runGroupsAreaChecks(adminUrl, scratchRoleUrl, results) {
|
||||||
const groupsMigrationSql = readGroupsRlsPoliciesMigrationSql();
|
const groupsMigrationSql = readGroupsRlsPoliciesMigrationSql();
|
||||||
const remainingMigrationSql = readRemainingTenantTablesMigrationSql();
|
const remainingMigrationSql = readRemainingTenantTablesMigrationSql();
|
||||||
|
const widenMigrationSql = readRlsWidenMigrationSql();
|
||||||
|
|
||||||
const groupPolicy = groupsMigrationSql ? extractPolicySql(groupsMigrationSql, 'Group') : null;
|
const groupPolicy = groupsMigrationSql ? extractPolicySql(groupsMigrationSql, 'Group') : null;
|
||||||
const groupMembershipPolicy = groupsMigrationSql
|
const groupMembershipPolicy = widenMigrationSql
|
||||||
? extractPolicySql(groupsMigrationSql, 'GroupMembership')
|
? extractPolicySql(widenMigrationSql, 'GroupMembership')
|
||||||
: null;
|
: null;
|
||||||
const moduleGrantPolicy = groupsMigrationSql
|
const moduleGrantPolicy = widenMigrationSql
|
||||||
? extractPolicySql(groupsMigrationSql, 'ModuleGrant')
|
? extractPolicySql(widenMigrationSql, 'ModuleGrant')
|
||||||
: null;
|
: null;
|
||||||
const tenantModuleActivationPolicy = remainingMigrationSql
|
const tenantModuleActivationPolicy = remainingMigrationSql
|
||||||
? extractPolicySql(remainingMigrationSql, 'TenantModuleActivation')
|
? extractPolicySql(remainingMigrationSql, 'TenantModuleActivation')
|
||||||
@@ -602,7 +645,7 @@ async function runGroupsAreaChecks(adminUrl, scratchRoleUrl, results) {
|
|||||||
results,
|
results,
|
||||||
'groups-policies-aus-migration-gefunden',
|
'groups-policies-aus-migration-gefunden',
|
||||||
false,
|
false,
|
||||||
'CREATE POLICY fuer "Group", "GroupMembership", "ModuleGrant" und/oder "TenantModuleActivation" nicht in den ausgelieferten Migrationen gefunden',
|
'CREATE POLICY fuer "Group" (20260804130918_groups_rls_policies), "GroupMembership"/"ModuleGrant" (20260910120000_rls_widen_membership_grant_and_platform_read) und/oder "TenantModuleActivation" (20260909140000_rls_remaining_tenant_tables) nicht in den ausgelieferten Migrationen gefunden',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -735,31 +778,63 @@ async function runGroupsAreaChecks(adminUrl, scratchRoleUrl, results) {
|
|||||||
foreignGroupInsertDetail,
|
foreignGroupInsertDetail,
|
||||||
);
|
);
|
||||||
|
|
||||||
// groupmembership-schreiben-fremder-benutzer-nicht-verhindert (Befund E):
|
// groupmembership-schreiben-fremder-benutzer-abgelehnt (260910-jab,
|
||||||
// das GELINGEN dieses INSERTs ist das bestandene Ergebnis — es belegt,
|
// T-JTS-02) — Umkehr der bisherigen loch-behauptenden Pruefung
|
||||||
// dass die Policy nur die Gruppenseite prueft, nicht die Benutzerseite.
|
// `groupmembership-schreiben-fremder-benutzer-nicht-verhindert`. Gemessen
|
||||||
let foreignUserInsertSucceeded = false;
|
// mit `user-b`, den es in TENANT-B TATSAECHLICH gibt (nicht mit einer
|
||||||
|
// erfundenen Kennung wie vormals `user-nicht-in-a`) — nur so misst diese
|
||||||
|
// Pruefung den Fall, den T-JTS-02 benannt hat, statt eines
|
||||||
|
// Fremdschluessel-Nichts. Die ABWEISUNG ist jetzt das bestandene
|
||||||
|
// Ergebnis: die Regel prueft seit 20260910120000_rls_widen_membership_
|
||||||
|
// grant_and_platform_read beide Seiten der Beziehung.
|
||||||
|
let foreignUserInsertRejected = false;
|
||||||
let foreignUserInsertDetail = '';
|
let foreignUserInsertDetail = '';
|
||||||
try {
|
try {
|
||||||
await forTenantQuery(
|
await forTenantQuery(
|
||||||
prisma,
|
prisma,
|
||||||
'TENANT-A',
|
'TENANT-A',
|
||||||
(tx) =>
|
(tx) =>
|
||||||
tx.$executeRaw`INSERT INTO "GroupMembership" (id, "groupId", "userId", source) VALUES ('membership-foreign-user', 'group-a', 'user-nicht-in-a', 'MANUAL')`,
|
tx.$executeRaw`INSERT INTO "GroupMembership" (id, "groupId", "userId", source) VALUES ('membership-foreign-user', 'group-a', 'user-b', 'MANUAL')`,
|
||||||
);
|
);
|
||||||
foreignUserInsertSucceeded = true;
|
|
||||||
foreignUserInsertDetail =
|
foreignUserInsertDetail =
|
||||||
'INSERT mit A-eigener Gruppe, aber einer Benutzerkennung, die es in A nicht gibt, ist GELUNGEN — die Policy auf GroupMembership prueft nur die Gruppenseite, nicht die Benutzerseite (Befund E); die Anwendung muss die Benutzerseite selbst pruefen';
|
'INSERT mit A-eigener Gruppe, aber einer Benutzerkennung aus TENANT-B (user-b) ist NICHT fehlgeschlagen';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
foreignUserInsertDetail = `INSERT unerwartet abgewiesen: ${err.message}`;
|
foreignUserInsertRejected = true;
|
||||||
|
foreignUserInsertDetail = `INSERT mit A-eigener Gruppe, aber fremder Benutzerkennung (user-b, TENANT-B) abgewiesen: ${err.message} — Umkehr von 'groupmembership-schreiben-fremder-benutzer-nicht-verhindert' (T-JTS-02, Befund E): die Policy auf GroupMembership prueft jetzt zusaetzlich die Benutzerseite, nicht mehr nur die Gruppenseite`;
|
||||||
}
|
}
|
||||||
report(
|
report(
|
||||||
results,
|
results,
|
||||||
'groupmembership-schreiben-fremder-benutzer-nicht-verhindert',
|
'groupmembership-schreiben-fremder-benutzer-abgelehnt',
|
||||||
foreignUserInsertSucceeded,
|
foreignUserInsertRejected,
|
||||||
foreignUserInsertDetail,
|
foreignUserInsertDetail,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// groupmembership-fremder-benutzer-ueber-die-wartungsrolle-weiterhin-moeglich
|
||||||
|
// — die Gegenmessung: dasselbe Einfuegen ueber die Verwaltungsrolle mit
|
||||||
|
// Umgehungsrecht GELINGT. Damit steht fest, dass die Abweisung oben von
|
||||||
|
// der Regel kommt und nicht vom Aufbau. Praezedenzfall:
|
||||||
|
// `gruppenpfad-ueber-die-wartungsrolle-liefert-die-fremde-gruppe-mit`.
|
||||||
|
let foreignUserInsertViaAdminSucceeded = false;
|
||||||
|
let foreignUserInsertViaAdminDetail = '';
|
||||||
|
try {
|
||||||
|
await withAdminPrisma(urlForDatabase(adminUrl, SCRATCH_DB_NAME).toString(), (db) =>
|
||||||
|
db.$executeRawUnsafe(
|
||||||
|
`INSERT INTO "GroupMembership" (id, "groupId", "userId", source) VALUES ('membership-foreign-user-admin', 'group-a', 'user-b', 'MANUAL')`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
foreignUserInsertViaAdminSucceeded = true;
|
||||||
|
foreignUserInsertViaAdminDetail =
|
||||||
|
'INSERT mit A-eigener Gruppe, aber fremder Benutzerkennung (user-b, TENANT-B) ist ueber die Wartungsrolle (BYPASSRLS) weiterhin GELUNGEN — der Ausschluss aus der vorigen Pruefung kommt damit nachweislich von der Regel, nicht vom Aufbau';
|
||||||
|
} catch (err) {
|
||||||
|
foreignUserInsertViaAdminDetail = `INSERT ueber die Wartungsrolle unerwartet abgewiesen: ${err.message}`;
|
||||||
|
}
|
||||||
|
report(
|
||||||
|
results,
|
||||||
|
'groupmembership-fremder-benutzer-ueber-die-wartungsrolle-weiterhin-moeglich',
|
||||||
|
foreignUserInsertViaAdminSucceeded,
|
||||||
|
foreignUserInsertViaAdminDetail,
|
||||||
|
);
|
||||||
|
|
||||||
// modulegrant-gebunden-nur-eigene-zeile
|
// modulegrant-gebunden-nur-eigene-zeile
|
||||||
const grantRowsForA = await forTenantQuery(prisma, 'TENANT-A', (tx) =>
|
const grantRowsForA = await forTenantQuery(prisma, 'TENANT-A', (tx) =>
|
||||||
tx.$queryRaw`SELECT "tenantId" FROM "ModuleGrant" ORDER BY id`,
|
tx.$queryRaw`SELECT "tenantId" FROM "ModuleGrant" ORDER BY id`,
|
||||||
@@ -771,30 +846,88 @@ async function runGroupsAreaChecks(adminUrl, scratchRoleUrl, results) {
|
|||||||
`forTenant(TENANT-A) liefert ${grantRowsForA.length} Zeile(n): ${JSON.stringify(grantRowsForA.map((r) => r.tenantId))}`,
|
`forTenant(TENANT-A) liefert ${grantRowsForA.length} Zeile(n): ${JSON.stringify(grantRowsForA.map((r) => r.tenantId))}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// modulegrant-fremde-gruppe-trotz-eigener-mandantenkennung-erlaubt
|
// modulegrant-fremde-gruppe-abgelehnt (260910-jab, T-JTS-03) — Umkehr
|
||||||
// (Befund F): auch hier ist das Durchgehen das bestandene Ergebnis.
|
// der bisherigen loch-behauptenden Pruefung
|
||||||
let foreignGroupGrantSucceeded = false;
|
// `modulegrant-fremde-gruppe-trotz-eigener-mandantenkennung-erlaubt`. Die
|
||||||
|
// ABWEISUNG ist jetzt das bestandene Ergebnis. Eigene id
|
||||||
|
// ('grant-foreign-group-rejected-attempt'), NICHT 'grant-foreign-group'
|
||||||
|
// — jene Kennung bleibt fuer die Zeile reserviert, die die Gegenmessung
|
||||||
|
// unten ueber die Wartungsrolle anlegt und auf der zwei Pruefungen des
|
||||||
|
// Bereichs `module-registry` aufsetzen (Befund C).
|
||||||
|
let foreignGroupGrantRejected = false;
|
||||||
let foreignGroupGrantDetail = '';
|
let foreignGroupGrantDetail = '';
|
||||||
try {
|
try {
|
||||||
await forTenantQuery(
|
await forTenantQuery(
|
||||||
prisma,
|
prisma,
|
||||||
'TENANT-A',
|
'TENANT-A',
|
||||||
(tx) =>
|
(tx) =>
|
||||||
tx.$executeRaw`INSERT INTO "ModuleGrant" (id, "tenantId", "moduleId", "groupId", "userId") VALUES ('grant-foreign-group', 'TENANT-A', 'mod-1', 'group-b', NULL)`,
|
tx.$executeRaw`INSERT INTO "ModuleGrant" (id, "tenantId", "moduleId", "groupId", "userId") VALUES ('grant-foreign-group-rejected-attempt', 'TENANT-A', 'mod-1', 'group-b', NULL)`,
|
||||||
);
|
);
|
||||||
foreignGroupGrantSucceeded = true;
|
foreignGroupGrantDetail = 'INSERT mit korrekter eigener tenantId, aber fremder groupId ist NICHT fehlgeschlagen';
|
||||||
foreignGroupGrantDetail =
|
|
||||||
'INSERT mit korrekter eigener tenantId, aber fremder groupId ist GELUNGEN — die Policy auf ModuleGrant prueft nur die Mandantenkennung der Zeile, nicht die referenzierte Gruppe (Befund F); assertTargetBelongsToTenant ist der einzige Schutz und darf bei der Umstellung nicht entfallen';
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
foreignGroupGrantDetail = `INSERT unerwartet abgewiesen: ${err.message}`;
|
foreignGroupGrantRejected = true;
|
||||||
|
foreignGroupGrantDetail = `INSERT mit korrekter eigener tenantId, aber fremder groupId (group-b, TENANT-B) abgewiesen: ${err.message} — Umkehr von 'modulegrant-fremde-gruppe-trotz-eigener-mandantenkennung-erlaubt' (T-JTS-03, Befund F): die Policy auf ModuleGrant prueft jetzt zusaetzlich, ob die referenzierte Gruppe zum Mandanten gehoert. assertTargetBelongsToTenant in module-grants.service.ts bleibt DAVON UNABHAENGIG bestehen — dieses zweite Netz wird durch die Regelaenderung nicht ersetzt`;
|
||||||
}
|
}
|
||||||
report(
|
report(
|
||||||
results,
|
results,
|
||||||
'modulegrant-fremde-gruppe-trotz-eigener-mandantenkennung-erlaubt',
|
'modulegrant-fremde-gruppe-abgelehnt',
|
||||||
foreignGroupGrantSucceeded,
|
foreignGroupGrantRejected,
|
||||||
foreignGroupGrantDetail,
|
foreignGroupGrantDetail,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// modulegrant-fremder-benutzer-abgelehnt — NEU, der zweite Zweig des
|
||||||
|
// Entweder-oder (D-04): eine Freigabe mit eigener Mandantenkennung und
|
||||||
|
// fremder Benutzerkennung. T-JTS-03 hat nur den Gruppenzweig gemessen;
|
||||||
|
// ohne diese Pruefung bliebe die Haelfte des Lochs offen.
|
||||||
|
let foreignUserGrantRejected = false;
|
||||||
|
let foreignUserGrantDetail = '';
|
||||||
|
try {
|
||||||
|
await forTenantQuery(
|
||||||
|
prisma,
|
||||||
|
'TENANT-A',
|
||||||
|
(tx) =>
|
||||||
|
tx.$executeRaw`INSERT INTO "ModuleGrant" (id, "tenantId", "moduleId", "groupId", "userId") VALUES ('grant-foreign-user-rejected-attempt', 'TENANT-A', 'mod-1', NULL, 'user-b')`,
|
||||||
|
);
|
||||||
|
foreignUserGrantDetail = 'INSERT mit korrekter eigener tenantId, aber fremder userId ist NICHT fehlgeschlagen';
|
||||||
|
} catch (err) {
|
||||||
|
foreignUserGrantRejected = true;
|
||||||
|
foreignUserGrantDetail = `INSERT mit korrekter eigener tenantId, aber fremder userId (user-b, TENANT-B) abgewiesen: ${err.message} — T-JTS-03 hatte nur den Gruppenzweig des Entweder-oder (D-04) gemessen; dieser Zweig ist der zweite und schliesst die andere Haelfte des Lochs`;
|
||||||
|
}
|
||||||
|
report(
|
||||||
|
results,
|
||||||
|
'modulegrant-fremder-benutzer-abgelehnt',
|
||||||
|
foreignUserGrantRejected,
|
||||||
|
foreignUserGrantDetail,
|
||||||
|
);
|
||||||
|
|
||||||
|
// modulegrant-fremde-gruppe-ueber-die-wartungsrolle-weiterhin-moeglich —
|
||||||
|
// Ersatz/Umkehr-Gegenmessung fuer die vormals loch-behauptende Pruefung.
|
||||||
|
// Legt ZUGLEICH die Zeile 'grant-foreign-group' an, die vormals als
|
||||||
|
// Nebenwirkung der loch-behauptenden Pruefung entstand und auf der die
|
||||||
|
// beiden Pruefungen des Bereichs `module-registry` aufsetzen (Befund C)
|
||||||
|
// — jetzt ueber die Wartungsrolle bereitgestellt, weil das gebundene
|
||||||
|
// INSERT oben abgewiesen wird.
|
||||||
|
let foreignGroupGrantViaAdminSucceeded = false;
|
||||||
|
let foreignGroupGrantViaAdminDetail = '';
|
||||||
|
try {
|
||||||
|
await withAdminPrisma(urlForDatabase(adminUrl, SCRATCH_DB_NAME).toString(), (db) =>
|
||||||
|
db.$executeRawUnsafe(
|
||||||
|
`INSERT INTO "ModuleGrant" (id, "tenantId", "moduleId", "groupId", "userId") VALUES ('grant-foreign-group', 'TENANT-A', 'mod-1', 'group-b', NULL)`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
foreignGroupGrantViaAdminSucceeded = true;
|
||||||
|
foreignGroupGrantViaAdminDetail =
|
||||||
|
"INSERT mit korrekter eigener tenantId, aber fremder groupId ist ueber die Wartungsrolle (BYPASSRLS) weiterhin GELUNGEN — Umkehr von 'modulegrant-fremde-gruppe-trotz-eigener-mandantenkennung-erlaubt' (T-JTS-03): die Regel weist diese Zeile unter gebundenem Kontext nachweislich ab (siehe 'modulegrant-fremde-gruppe-abgelehnt'), die Wartungsrolle umgeht RLS grundsaetzlich; legt zugleich die Zeile 'grant-foreign-group' an, auf der zwei Pruefungen des Bereichs module-registry aufsetzen (Befund C)";
|
||||||
|
} catch (err) {
|
||||||
|
foreignGroupGrantViaAdminDetail = `INSERT ueber die Wartungsrolle unerwartet abgewiesen: ${err.message}`;
|
||||||
|
}
|
||||||
|
report(
|
||||||
|
results,
|
||||||
|
'modulegrant-fremde-gruppe-ueber-die-wartungsrolle-weiterhin-moeglich',
|
||||||
|
foreignGroupGrantViaAdminSucceeded,
|
||||||
|
foreignGroupGrantViaAdminDetail,
|
||||||
|
);
|
||||||
|
|
||||||
// tenantmoduleactivation-gebunden-nur-eigene-zeile
|
// tenantmoduleactivation-gebunden-nur-eigene-zeile
|
||||||
const activationRowsForA = await forTenantQuery(prisma, 'TENANT-A', (tx) =>
|
const activationRowsForA = await forTenantQuery(prisma, 'TENANT-A', (tx) =>
|
||||||
tx.$queryRaw`SELECT "tenantId" FROM "TenantModuleActivation" ORDER BY id`,
|
tx.$queryRaw`SELECT "tenantId" FROM "TenantModuleActivation" ORDER BY id`,
|
||||||
@@ -811,13 +944,19 @@ async function runGroupsAreaChecks(adminUrl, scratchRoleUrl, results) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aufgabe 1 (260909-laa) — misst die drei Sonderfaelle des Bereichs tenders
|
* Aufgabe 1 (260909-laa/260910-jab) — misst die Sonderfaelle des Bereichs
|
||||||
* unter der Rolle ohne BYPASSRLS, mit den fuenf Policies WORTGLEICH aus der
|
* tenders unter der Rolle ohne BYPASSRLS. Vier der fuenf Policies
|
||||||
* ausgelieferten Migration `_rls_remaining_tenant_tables` (dieselbe Datei,
|
* (TenderEmailConfig, TenderNotificationPref, TenderSavedSearch,
|
||||||
* die bereits die TenantModuleActivation-Policy fuer runGroupsAreaChecks
|
* TenderTriage) kommen WORTGLEICH aus der ausgelieferten Migration
|
||||||
* liefert). Findet die Extraktion eine der fuenf nicht, meldet dieser
|
* `_rls_remaining_tenant_tables` (dieselbe Datei, die bereits die
|
||||||
* Abschnitt eine FEHLGESCHLAGENE Pruefung und bricht ab, statt mit einer
|
* TenantModuleActivation-Policy fuer runGroupsAreaChecks liefert).
|
||||||
* geratenen Policy weiterzumessen.
|
* TenderRssFeedSource (WINDOWS #19) kommt seit 260910-jab NICHT mehr aus
|
||||||
|
* jener Datei — die dortige Fassung ist die abgeloeste, kuerzer greifende
|
||||||
|
* Regel (Befund D) — sondern als VIER nach Befehl getrennte Policies aus
|
||||||
|
* `readRlsWidenMigrationSql()`, extrahiert mit `extractAllPolicySql()`
|
||||||
|
* (mehrere Treffer je Tabelle, Befund D). Findet die Extraktion eine der
|
||||||
|
* Policies nicht, meldet dieser Abschnitt eine FEHLGESCHLAGENE Pruefung und
|
||||||
|
* bricht ab, statt mit einer geratenen Policy weiterzumessen.
|
||||||
*
|
*
|
||||||
* Legt keine Tabelle an, auf der eine andere Pruefung dieses Werkzeugs
|
* Legt keine Tabelle an, auf der eine andere Pruefung dieses Werkzeugs
|
||||||
* aufsetzt — anders als runGroupsAreaChecks() (Vorbedingung fuer
|
* aufsetzt — anders als runGroupsAreaChecks() (Vorbedingung fuer
|
||||||
@@ -829,6 +968,7 @@ async function runGroupsAreaChecks(adminUrl, scratchRoleUrl, results) {
|
|||||||
*/
|
*/
|
||||||
async function runTendersAreaChecks(adminUrl, scratchRoleUrl, results) {
|
async function runTendersAreaChecks(adminUrl, scratchRoleUrl, results) {
|
||||||
const remainingMigrationSql = readRemainingTenantTablesMigrationSql();
|
const remainingMigrationSql = readRemainingTenantTablesMigrationSql();
|
||||||
|
const widenMigrationSql = readRlsWidenMigrationSql();
|
||||||
|
|
||||||
const emailConfigPolicy = remainingMigrationSql
|
const emailConfigPolicy = remainingMigrationSql
|
||||||
? extractPolicySql(remainingMigrationSql, 'TenderEmailConfig')
|
? extractPolicySql(remainingMigrationSql, 'TenderEmailConfig')
|
||||||
@@ -836,9 +976,9 @@ async function runTendersAreaChecks(adminUrl, scratchRoleUrl, results) {
|
|||||||
const notificationPrefPolicy = remainingMigrationSql
|
const notificationPrefPolicy = remainingMigrationSql
|
||||||
? extractPolicySql(remainingMigrationSql, 'TenderNotificationPref')
|
? extractPolicySql(remainingMigrationSql, 'TenderNotificationPref')
|
||||||
: null;
|
: null;
|
||||||
const rssFeedPolicy = remainingMigrationSql
|
const rssFeedPolicies = widenMigrationSql
|
||||||
? extractPolicySql(remainingMigrationSql, 'TenderRssFeedSource')
|
? extractAllPolicySql(widenMigrationSql, 'TenderRssFeedSource')
|
||||||
: null;
|
: [];
|
||||||
const savedSearchPolicy = remainingMigrationSql
|
const savedSearchPolicy = remainingMigrationSql
|
||||||
? extractPolicySql(remainingMigrationSql, 'TenderSavedSearch')
|
? extractPolicySql(remainingMigrationSql, 'TenderSavedSearch')
|
||||||
: null;
|
: null;
|
||||||
@@ -849,7 +989,7 @@ async function runTendersAreaChecks(adminUrl, scratchRoleUrl, results) {
|
|||||||
if (
|
if (
|
||||||
!emailConfigPolicy ||
|
!emailConfigPolicy ||
|
||||||
!notificationPrefPolicy ||
|
!notificationPrefPolicy ||
|
||||||
!rssFeedPolicy ||
|
rssFeedPolicies.length !== 4 ||
|
||||||
!savedSearchPolicy ||
|
!savedSearchPolicy ||
|
||||||
!triagePolicy
|
!triagePolicy
|
||||||
) {
|
) {
|
||||||
@@ -857,7 +997,7 @@ async function runTendersAreaChecks(adminUrl, scratchRoleUrl, results) {
|
|||||||
results,
|
results,
|
||||||
'tenders-policies-aus-migration-gefunden',
|
'tenders-policies-aus-migration-gefunden',
|
||||||
false,
|
false,
|
||||||
'CREATE POLICY fuer "TenderEmailConfig", "TenderNotificationPref", "TenderRssFeedSource", "TenderSavedSearch" und/oder "TenderTriage" nicht in der ausgelieferten *_rls_remaining_tenant_tables-Migration gefunden',
|
`CREATE POLICY fuer "TenderEmailConfig", "TenderNotificationPref", "TenderSavedSearch" und/oder "TenderTriage" nicht in der ausgelieferten *_rls_remaining_tenant_tables-Migration gefunden, oder nicht genau vier Policies fuer "TenderRssFeedSource" in *_rls_widen_membership_grant_and_platform_read (gefunden: ${rssFeedPolicies.length})`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -917,7 +1057,9 @@ async function runTendersAreaChecks(adminUrl, scratchRoleUrl, results) {
|
|||||||
}
|
}
|
||||||
await db.$executeRawUnsafe(emailConfigPolicy);
|
await db.$executeRawUnsafe(emailConfigPolicy);
|
||||||
await db.$executeRawUnsafe(notificationPrefPolicy);
|
await db.$executeRawUnsafe(notificationPrefPolicy);
|
||||||
await db.$executeRawUnsafe(rssFeedPolicy);
|
for (const policySql of rssFeedPolicies) {
|
||||||
|
await db.$executeRawUnsafe(policySql);
|
||||||
|
}
|
||||||
await db.$executeRawUnsafe(savedSearchPolicy);
|
await db.$executeRawUnsafe(savedSearchPolicy);
|
||||||
await db.$executeRawUnsafe(triagePolicy);
|
await db.$executeRawUnsafe(triagePolicy);
|
||||||
|
|
||||||
@@ -1038,13 +1180,13 @@ async function runTendersAreaChecks(adminUrl, scratchRoleUrl, results) {
|
|||||||
`forTenant(TENANT-A) liefert ${notificationPrefRowsForA.length} Zeile(n): ${JSON.stringify(notificationPrefRowsForA.map((r) => r.tenantId))}`,
|
`forTenant(TENANT-A) liefert ${notificationPrefRowsForA.length} Zeile(n): ${JSON.stringify(notificationPrefRowsForA.map((r) => r.tenantId))}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// tenderrssfeed-plattformzeile-unter-jedem-mandanten-unsichtbar
|
// tenderrssfeed-plattformzeile-gebunden-sichtbar (260910-jab, WINDOWS
|
||||||
// (WINDOWS #19): die plattformweite Zeile (userId/tenantId NULL) ist
|
// #19) — Umkehr von 'tenderrssfeed-plattformzeile-unter-jedem-
|
||||||
// unter BEIDEN Mandantenkontexten unsichtbar, weil die ausgelieferte
|
// mandanten-unsichtbar': die plattformweite Zeile (userId/tenantId
|
||||||
// Policy "tenantId" = current_tenant_id() NULL nie gleich vergleicht.
|
// NULL) ist jetzt unter BEIDEN Mandantenkontexten SICHTBAR, weil die
|
||||||
// Bestanden, wenn sie unter beiden Kontexten fehlt — die Folge: die
|
// Leseregel der neuen vier Policies (tenant_platform_read_policy,
|
||||||
// drei RSS-Pfade, die plattformweite Zeilen beruehren, duerfen nicht
|
// 20260910120000_rls_widen_membership_grant_and_platform_read) Zeilen
|
||||||
// gebunden werden, solange die Policy-Semantik unveraendert ist.
|
// ohne Mandant ausdruecklich einschliesst.
|
||||||
const rssRowsForA = await forTenantQuery(prisma, 'TENANT-A', (tx) =>
|
const rssRowsForA = await forTenantQuery(prisma, 'TENANT-A', (tx) =>
|
||||||
tx.$queryRaw`SELECT id FROM "TenderRssFeedSource" ORDER BY id`,
|
tx.$queryRaw`SELECT id FROM "TenderRssFeedSource" ORDER BY id`,
|
||||||
);
|
);
|
||||||
@@ -1055,15 +1197,47 @@ async function runTendersAreaChecks(adminUrl, scratchRoleUrl, results) {
|
|||||||
const platformRowVisibleForB = rssRowsForB.some((r) => r.id === 'rss-platform');
|
const platformRowVisibleForB = rssRowsForB.some((r) => r.id === 'rss-platform');
|
||||||
report(
|
report(
|
||||||
results,
|
results,
|
||||||
'tenderrssfeed-plattformzeile-unter-jedem-mandanten-unsichtbar',
|
'tenderrssfeed-plattformzeile-gebunden-sichtbar',
|
||||||
!platformRowVisibleForA && !platformRowVisibleForB,
|
platformRowVisibleForA && platformRowVisibleForB,
|
||||||
`forTenant(TENANT-A) sieht die Platform-Zeile: ${platformRowVisibleForA}, forTenant(TENANT-B) sieht sie: ${platformRowVisibleForB} — WINDOWS #19: eine plattformweite RSS-Quelle ist unter JEDEM Mandantenkontext unsichtbar; listForUser/createPlatform/remove duerfen deshalb nicht gebunden werden, solange die Policy-Semantik unveraendert ist`,
|
`forTenant(TENANT-A) sieht die Platform-Zeile: ${platformRowVisibleForA}, forTenant(TENANT-B) sieht sie: ${platformRowVisibleForB} — Umkehr von 'tenderrssfeed-plattformzeile-unter-jedem-mandanten-unsichtbar' (WINDOWS #19): eine plattformweite RSS-Quelle ist seit 20260910120000_rls_widen_membership_grant_and_platform_read unter JEDEM Mandantenkontext sichtbar`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// tenderrssfeed-eigene-zeile-gebunden-weiterhin-sichtbar — die andere
|
||||||
|
// Fehlerrichtung derselben Aenderung: der gebundene Lesezugriff liefert
|
||||||
|
// ZUSAETZLICH weiterhin die eigene Zeile des Mandanten und NICHT die des
|
||||||
|
// anderen. Ohne diese Pruefung waere eine zu weit gefasste Leseregel
|
||||||
|
// unbemerkt geblieben.
|
||||||
|
const ownRowVisibleForA = rssRowsForA.some((r) => r.id === 'rss-a');
|
||||||
|
const foreignRowVisibleForA = rssRowsForA.some((r) => r.id === 'rss-b');
|
||||||
|
report(
|
||||||
|
results,
|
||||||
|
'tenderrssfeed-eigene-zeile-gebunden-weiterhin-sichtbar',
|
||||||
|
ownRowVisibleForA && !foreignRowVisibleForA,
|
||||||
|
`forTenant(TENANT-A) liefert ${JSON.stringify(rssRowsForA.map((r) => r.id))} — eigene Zeile (rss-a) sichtbar: ${ownRowVisibleForA}, fremde Zeile (rss-b, TENANT-B) sichtbar: ${foreignRowVisibleForA}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// tenderrssfeed-ungebunden-nur-die-plattformzeile — die Belegzeile fuer
|
||||||
|
// Befund F: ohne gesetzten Mandantenkontext liefert der Lesezugriff
|
||||||
|
// jetzt genau die plattformweiten Zeilen und keine persoenliche. Das ist
|
||||||
|
// die neue Fehlerrichtung, die Aufgabe 2 traegt (listForUser wird
|
||||||
|
// gebunden); hier gemessen, nicht behauptet.
|
||||||
|
const unboundRssRows = await prisma.$queryRaw`SELECT id FROM "TenderRssFeedSource" ORDER BY id`;
|
||||||
|
const unboundOnlyPlatform =
|
||||||
|
unboundRssRows.length === 1 && unboundRssRows[0].id === 'rss-platform';
|
||||||
|
report(
|
||||||
|
results,
|
||||||
|
'tenderrssfeed-ungebunden-nur-die-plattformzeile',
|
||||||
|
unboundOnlyPlatform,
|
||||||
|
`ungebundener SELECT auf "TenderRssFeedSource" liefert ${unboundRssRows.length} Zeile(n): ${JSON.stringify(unboundRssRows.map((r) => r.id))} — Befund F: aus einer schreienden Leere (vor der Reparatur: null Zeilen) wuerde nach dem Scharfschalten eine kurze, glaubhafte Teilantwort (nur die plattformweiten Zeilen); listForUser() wird deshalb in Aufgabe 2 gebunden`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// tenderrssfeed-gebundenes-einfuegen-ohne-mandant-abgelehnt: ein
|
// tenderrssfeed-gebundenes-einfuegen-ohne-mandant-abgelehnt: ein
|
||||||
// gebundenes INSERT unter TENANT-A mit tenantId=NULL wird abgewiesen —
|
// gebundenes INSERT unter TENANT-A mit tenantId=NULL wird abgewiesen —
|
||||||
// die Abweisung IST das bestandene Ergebnis. Folge: createPlatform darf
|
// die Abweisung IST das bestandene Ergebnis. Folge: createPlatform darf
|
||||||
// nicht gebunden werden.
|
// nicht gebunden werden. Die Abweisung kommt jetzt von der ausdruecklichen
|
||||||
|
// WITH-CHECK-Klausel der Einfuegeregel (tenant_insert_policy), nicht mehr
|
||||||
|
// von der stillschweigenden Zweitverwendung eines einzigen USING-
|
||||||
|
// Ausdrucks.
|
||||||
let platformInsertRejected = false;
|
let platformInsertRejected = false;
|
||||||
let platformInsertDetail = '';
|
let platformInsertDetail = '';
|
||||||
try {
|
try {
|
||||||
@@ -1076,7 +1250,7 @@ async function runTendersAreaChecks(adminUrl, scratchRoleUrl, results) {
|
|||||||
platformInsertDetail = 'gebundenes INSERT mit tenantId=NULL ist NICHT fehlgeschlagen';
|
platformInsertDetail = 'gebundenes INSERT mit tenantId=NULL ist NICHT fehlgeschlagen';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
platformInsertRejected = true;
|
platformInsertRejected = true;
|
||||||
platformInsertDetail = `gebundenes INSERT mit tenantId=NULL abgewiesen: ${err.message} — createPlatform darf deshalb nicht gebunden werden`;
|
platformInsertDetail = `gebundenes INSERT mit tenantId=NULL abgewiesen (tenant_insert_policy): ${err.message} — createPlatform darf deshalb nicht gebunden werden`;
|
||||||
}
|
}
|
||||||
report(
|
report(
|
||||||
results,
|
results,
|
||||||
@@ -1085,6 +1259,75 @@ async function runTendersAreaChecks(adminUrl, scratchRoleUrl, results) {
|
|||||||
platformInsertDetail,
|
platformInsertDetail,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// tenderrssfeed-gebundenes-aendern-der-plattformzeile-abgelehnt — NEU:
|
||||||
|
// ein gebundenes UPDATE auf die plattformweite Zeile wird abgewiesen.
|
||||||
|
// Zusammen mit der folgenden Pruefung die Messung der Richtung "zu
|
||||||
|
// locker" (Befund K) und damit der eigentliche Grund fuer die Trennung
|
||||||
|
// nach Befehl: eine einzelne Leseregel, die die plattformweite Zeile
|
||||||
|
// einschliesst, wuerde ohne diese Trennung auch UPDATE/DELETE erlauben.
|
||||||
|
let platformUpdateRejected = false;
|
||||||
|
let platformUpdateDetail = '';
|
||||||
|
try {
|
||||||
|
await forTenantQuery(
|
||||||
|
prisma,
|
||||||
|
'TENANT-A',
|
||||||
|
(tx) =>
|
||||||
|
tx.$executeRaw`UPDATE "TenderRssFeedSource" SET url = 'https://gebunden-veraendert.example-tenders.invalid/feed' WHERE id = 'rss-platform'`,
|
||||||
|
);
|
||||||
|
// Ein UPDATE, das keine Zeile trifft, wirft in Postgres KEINEN Fehler
|
||||||
|
// — es muss deshalb explizit nachgesehen werden, ob die Zeile
|
||||||
|
// tatsaechlich unveraendert blieb (die USING-Klausel der
|
||||||
|
// Aenderungsregel filtert die Zielzeile heraus, bevor SET greift).
|
||||||
|
const [afterUpdate] = await withAdminPrisma(
|
||||||
|
urlForDatabase(adminUrl, SCRATCH_DB_NAME).toString(),
|
||||||
|
(db) =>
|
||||||
|
db.$queryRaw`SELECT url FROM "TenderRssFeedSource" WHERE id = 'rss-platform'`,
|
||||||
|
);
|
||||||
|
platformUpdateRejected =
|
||||||
|
afterUpdate.url !== 'https://gebunden-veraendert.example-tenders.invalid/feed';
|
||||||
|
platformUpdateDetail = platformUpdateRejected
|
||||||
|
? `gebundenes UPDATE auf die plattformweite Zeile traf keine Zeile (tenant_update_policy filtert sie heraus) — url unveraendert: ${JSON.stringify(afterUpdate.url)}`
|
||||||
|
: `gebundenes UPDATE auf die plattformweite Zeile ist UNERWARTET durchgegangen — url jetzt: ${JSON.stringify(afterUpdate.url)}`;
|
||||||
|
} catch (err) {
|
||||||
|
platformUpdateRejected = true;
|
||||||
|
platformUpdateDetail = `gebundenes UPDATE auf die plattformweite Zeile abgewiesen: ${err.message}`;
|
||||||
|
}
|
||||||
|
report(
|
||||||
|
results,
|
||||||
|
'tenderrssfeed-gebundenes-aendern-der-plattformzeile-abgelehnt',
|
||||||
|
platformUpdateRejected,
|
||||||
|
platformUpdateDetail,
|
||||||
|
);
|
||||||
|
|
||||||
|
// tenderrssfeed-gebundenes-loeschen-der-plattformzeile-abgelehnt — NEU:
|
||||||
|
// ein gebundenes DELETE auf die plattformweite Zeile wird abgewiesen.
|
||||||
|
let platformDeleteRejected = false;
|
||||||
|
let platformDeleteDetail = '';
|
||||||
|
try {
|
||||||
|
const deleteResult = await forTenantQuery(
|
||||||
|
prisma,
|
||||||
|
'TENANT-A',
|
||||||
|
(tx) =>
|
||||||
|
tx.$executeRaw`DELETE FROM "TenderRssFeedSource" WHERE id = 'rss-platform'`,
|
||||||
|
);
|
||||||
|
// $executeRaw liefert die Anzahl betroffener Zeilen — 0, wenn die
|
||||||
|
// USING-Klausel der Loeschregel die Zielzeile herausfiltert, bevor
|
||||||
|
// DELETE greift (kein Fehler, wie bei UPDATE oben).
|
||||||
|
platformDeleteRejected = deleteResult === 0;
|
||||||
|
platformDeleteDetail = platformDeleteRejected
|
||||||
|
? 'gebundenes DELETE auf die plattformweite Zeile traf 0 Zeilen (tenant_delete_policy filtert sie heraus)'
|
||||||
|
: `gebundenes DELETE auf die plattformweite Zeile ist UNERWARTET durchgegangen (${deleteResult} Zeile(n) betroffen)`;
|
||||||
|
} catch (err) {
|
||||||
|
platformDeleteRejected = true;
|
||||||
|
platformDeleteDetail = `gebundenes DELETE auf die plattformweite Zeile abgewiesen: ${err.message}`;
|
||||||
|
}
|
||||||
|
report(
|
||||||
|
results,
|
||||||
|
'tenderrssfeed-gebundenes-loeschen-der-plattformzeile-abgelehnt',
|
||||||
|
platformDeleteRejected,
|
||||||
|
platformDeleteDetail,
|
||||||
|
);
|
||||||
|
|
||||||
// tendertriage-einfuegen-auf-unsichtbare-zeile-verletzt-eindeutigkeit
|
// tendertriage-einfuegen-auf-unsichtbare-zeile-verletzt-eindeutigkeit
|
||||||
// (Befund F): unter TENANT-A ein INSERT fuer ein Paar (userId,
|
// (Befund F): unter TENANT-A ein INSERT fuer ein Paar (userId,
|
||||||
// tenderId), dessen Zeile existiert, aber zu TENANT-B gehoert und daher
|
// tenderId), dessen Zeile existiert, aber zu TENANT-B gehoert und daher
|
||||||
@@ -1119,6 +1362,84 @@ async function runTendersAreaChecks(adminUrl, scratchRoleUrl, results) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aufgabe 1 (260910-jab) — misst, dass die UNVERAENDERT ausgelieferte Regel
|
||||||
|
* auf "SearchProvider" (20260909140000_rls_remaining_tenant_tables) eine
|
||||||
|
* mandantenlose Zeile weiterhin unter JEDEM Kontext unsichtbar laesst.
|
||||||
|
* Anders als bei TenderRssFeedSource ist das hier KEIN Loch, sondern
|
||||||
|
* korrektes Verhalten (Befund E): die Praemisse von WINDOWS #19 ist fuer
|
||||||
|
* diese Tabelle widerlegt — es gibt keinen Codeweg, der eine mandantenlose
|
||||||
|
* Zeile erzeugt (der einzige Schreibweg, dashboard.service.ts, verlangt die
|
||||||
|
* Mandantenkennung als Pflichtparameter; die Vorgabe-Suchmaschinen sind
|
||||||
|
* Konstanten, Entscheidung 05-02, keine Datenbankzeilen). Eine Lockerung
|
||||||
|
* hier waere die falsche Richtung. Eigene Wegwerf-Tabelle, weil keine
|
||||||
|
* andere Pruefung dieses Werkzeugs "SearchProvider" beruehrt.
|
||||||
|
*
|
||||||
|
* Legt keine Tabelle an, auf der eine andere Pruefung dieses Werkzeugs
|
||||||
|
* aufsetzt — wie runTendersAreaChecks() ist dieser Abschnitt in der
|
||||||
|
* Aufrufkette ein Blatt.
|
||||||
|
*/
|
||||||
|
async function runSearchProviderAreaChecks(adminUrl, scratchRoleUrl, results) {
|
||||||
|
const remainingMigrationSql = readRemainingTenantTablesMigrationSql();
|
||||||
|
const searchProviderPolicy = remainingMigrationSql
|
||||||
|
? extractPolicySql(remainingMigrationSql, 'SearchProvider')
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!searchProviderPolicy) {
|
||||||
|
report(
|
||||||
|
results,
|
||||||
|
'searchprovider-policy-aus-migration-gefunden',
|
||||||
|
false,
|
||||||
|
'CREATE POLICY fuer "SearchProvider" nicht in der ausgelieferten *_rls_remaining_tenant_tables-Migration gefunden',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await withAdminPrisma(urlForDatabase(adminUrl, SCRATCH_DB_NAME).toString(), async (db) => {
|
||||||
|
await db.$executeRawUnsafe(`
|
||||||
|
CREATE TABLE "SearchProvider" (
|
||||||
|
id text PRIMARY KEY,
|
||||||
|
"userId" text,
|
||||||
|
"tenantId" text,
|
||||||
|
name text NOT NULL
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
await db.$executeRawUnsafe(`ALTER TABLE "SearchProvider" ENABLE ROW LEVEL SECURITY;`);
|
||||||
|
await db.$executeRawUnsafe(`ALTER TABLE "SearchProvider" FORCE ROW LEVEL SECURITY;`);
|
||||||
|
await db.$executeRawUnsafe(searchProviderPolicy);
|
||||||
|
await db.$executeRawUnsafe(
|
||||||
|
`GRANT SELECT, INSERT, UPDATE, DELETE ON "SearchProvider" TO ${SCRATCH_ROLE_NAME}`,
|
||||||
|
);
|
||||||
|
// Eine mandantenlose Zeile wird ueber die Wartungsrolle angelegt — unter
|
||||||
|
// der Anwendungsrolle liesse sie sich mit der unveraenderten Regel
|
||||||
|
// ohnehin nicht schreiben.
|
||||||
|
await db.$executeRawUnsafe(`
|
||||||
|
INSERT INTO "SearchProvider" (id, "userId", "tenantId", name) VALUES
|
||||||
|
('search-tenantless', NULL, NULL, 'Mandantenlose Suchmaschine');
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const prisma = new PrismaClient({ datasourceUrl: scratchRoleUrl });
|
||||||
|
try {
|
||||||
|
const rowsForA = await forTenantQuery(prisma, 'TENANT-A', (tx) =>
|
||||||
|
tx.$queryRaw`SELECT id FROM "SearchProvider" ORDER BY id`,
|
||||||
|
);
|
||||||
|
const rowsForB = await forTenantQuery(prisma, 'TENANT-B', (tx) =>
|
||||||
|
tx.$queryRaw`SELECT id FROM "SearchProvider" ORDER BY id`,
|
||||||
|
);
|
||||||
|
const visibleForA = rowsForA.some((r) => r.id === 'search-tenantless');
|
||||||
|
const visibleForB = rowsForB.some((r) => r.id === 'search-tenantless');
|
||||||
|
report(
|
||||||
|
results,
|
||||||
|
'searchprovider-mandantenlose-zeile-bleibt-unter-jedem-kontext-unsichtbar',
|
||||||
|
!visibleForA && !visibleForB,
|
||||||
|
`forTenant(TENANT-A) sieht die mandantenlose Zeile: ${visibleForA}, forTenant(TENANT-B) sieht sie: ${visibleForB} — bewusst UNVERAENDERT (WINDOWS #19, Befund E: die Praemisse einer mandantenlosen SearchProvider-Zeile ist widerlegt, es gibt keinen Codeweg, der eine solche Zeile erzeugt — dashboard.service.ts verlangt die Mandantenkennung als Pflichtparameter, die Vorgabe-Suchmaschinen sind Konstanten, 05-02). Neu zu bewerten, WENN ein Schreibweg entsteht, der "tenantId" absichtlich leer laesst`,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aufgabe 1 (260909-mir), TEIL 1 — misst die neun im Plan genannten
|
* Aufgabe 1 (260909-mir), TEIL 1 — misst die neun im Plan genannten
|
||||||
* Verhaltensweisen des Bereichs dkv unter der Rolle ohne BYPASSRLS, mit
|
* Verhaltensweisen des Bereichs dkv unter der Rolle ohne BYPASSRLS, mit
|
||||||
@@ -1925,13 +2246,20 @@ async function runModuleRegistryAreaChecks(adminUrl, scratchRoleUrl, results) {
|
|||||||
`forTenant(TENANT-A) liefert ueber den Drei-Tabellen-Weg (Freigabe ueber Gruppe ueber Mitgliedschaft) fuer user-a ${groupPathForA.length} Zeile(n): ${JSON.stringify(groupPathForA.map((r) => r.id))}`,
|
`forTenant(TENANT-A) liefert ueber den Drei-Tabellen-Weg (Freigabe ueber Gruppe ueber Mitgliedschaft) fuer user-a ${groupPathForA.length} Zeile(n): ${JSON.stringify(groupPathForA.map((r) => r.id))}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 6: gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus
|
// 6: gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus — Meldetext
|
||||||
|
// seit 260910-jab RICHTIGGESTELLT: er behauptete vormals, die Regel auf
|
||||||
|
// "ModuleGrant" lasse 'grant-foreign-group' durch (T-JTS-03). Das ist
|
||||||
|
// nach der Reparatur unwahr — die Regel weist ein gebundenes Einfuegen
|
||||||
|
// dieser Zeile jetzt nachweislich ab (siehe 'modulegrant-fremde-gruppe-
|
||||||
|
// abgelehnt'); die Zeile existiert hier nur, weil
|
||||||
|
// 'modulegrant-fremde-gruppe-ueber-die-wartungsrolle-weiterhin-moeglich'
|
||||||
|
// sie ueber die Wartungsrolle (BYPASSRLS) angelegt hat.
|
||||||
const excludesForeignGroup = !groupPathForA.some((r) => r.id === 'grant-foreign-group');
|
const excludesForeignGroup = !groupPathForA.some((r) => r.id === 'grant-foreign-group');
|
||||||
report(
|
report(
|
||||||
results,
|
results,
|
||||||
'gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus',
|
'gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus',
|
||||||
excludesForeignGroup,
|
excludesForeignGroup,
|
||||||
`forTenant(TENANT-A) liefert 'grant-foreign-group' ueber denselben Drei-Tabellen-Weg NICHT (Ergebnis: ${JSON.stringify(groupPathForA.map((r) => r.id))}), obwohl die Regel auf "ModuleGrant" diese Zeile nachweislich durchlaesst (T-JTS-03) und die Mitgliedschaft (group-b, user-a) vorhanden ist — diese Verteidigung greift erst nach dem Scharfschalten`,
|
`forTenant(TENANT-A) liefert 'grant-foreign-group' ueber denselben Drei-Tabellen-Weg NICHT (Ergebnis: ${JSON.stringify(groupPathForA.map((r) => r.id))}) — die Zeile wurde ueber die Wartungsrolle angelegt (die Regel auf "ModuleGrant" weist ihr gebundenes Einfuegen seit T-JTS-02/T-JTS-03 nachweislich ab) und die Mitgliedschaft (group-b, user-a) ist vorhanden — diese Verteidigung greift erst nach dem Scharfschalten`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 7: gruppenpfad-ueber-die-wartungsrolle-liefert-die-fremde-gruppe-mit —
|
// 7: gruppenpfad-ueber-die-wartungsrolle-liefert-die-fremde-gruppe-mit —
|
||||||
@@ -2296,6 +2624,7 @@ async function main() {
|
|||||||
await runLdapAreaChecks(adminUrl, scratchRoleUrlString, results);
|
await runLdapAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||||
await runGroupsAreaChecks(adminUrl, scratchRoleUrlString, results);
|
await runGroupsAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||||
await runTendersAreaChecks(adminUrl, scratchRoleUrlString, results);
|
await runTendersAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||||
|
await runSearchProviderAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||||
await runDkvAreaChecks(adminUrl, scratchRoleUrlString, results);
|
await runDkvAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||||
await runUserAreaChecks(adminUrl, scratchRoleUrlString, results);
|
await runUserAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||||
await runModuleRegistryAreaChecks(adminUrl, scratchRoleUrlString, results);
|
await runModuleRegistryAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||||
|
|||||||
@@ -26,6 +26,24 @@ function readMigrationSql(suffix: string): string {
|
|||||||
return readFileSync(join(MIGRATIONS_DIR, dirs[0], 'migration.sql'), 'utf-8');
|
return readFileSync(join(MIGRATIONS_DIR, dirs[0], 'migration.sql'), 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schneidet eine einzelne `CREATE POLICY <name?> ON "<Tabelle>" ... ;`
|
||||||
|
* -Anweisung aus dem Migrationstext, wortgleich zu `extractPolicySql()` /
|
||||||
|
* `extractAllPolicySql()` in apps/api/scripts/rls-scratch-check.mjs — reiner
|
||||||
|
* Textabgleich, keine Datenbank. Ohne `policyName` wird der erste Treffer
|
||||||
|
* fuer die Tabelle genommen (fuer Tabellen mit genau einer Regel);
|
||||||
|
* `policyName` waehlt gezielt eine von mehreren (TenderRssFeedSource).
|
||||||
|
*/
|
||||||
|
function extractPolicyBlock(sql: string, tableName: string, policyName?: string): string {
|
||||||
|
const name = policyName ?? '\\w+';
|
||||||
|
const re = new RegExp(`CREATE POLICY ${name} ON "${tableName}"[\\s\\S]*?;`);
|
||||||
|
const match = sql.match(re);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`CREATE POLICY fuer "${tableName}"${policyName ? ` (${policyName})` : ''} nicht gefunden`);
|
||||||
|
}
|
||||||
|
return match[0];
|
||||||
|
}
|
||||||
|
|
||||||
describe('add_groups_and_module_grants migration.sql (D-04, D-06, D-13)', () => {
|
describe('add_groups_and_module_grants migration.sql (D-04, D-06, D-13)', () => {
|
||||||
const sql = readMigrationSql('_add_groups_and_module_grants');
|
const sql = readMigrationSql('_add_groups_and_module_grants');
|
||||||
|
|
||||||
@@ -95,6 +113,66 @@ describe('groups_rls_policies migration.sql (T-15-11)', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('rls_widen_membership_grant_and_platform_read migration.sql (T-JTS-02, T-JTS-03, WINDOWS #19)', () => {
|
||||||
|
const sql = readMigrationSql('_rls_widen_membership_grant_and_platform_read');
|
||||||
|
|
||||||
|
it('loest die abgeloesten Regeln auf GroupMembership und ModuleGrant ab (DROP + CREATE unter demselben Namen)', () => {
|
||||||
|
expect(sql).toContain('DROP POLICY tenant_isolation_policy ON "GroupMembership"');
|
||||||
|
expect(sql).toContain('DROP POLICY tenant_isolation_policy ON "ModuleGrant"');
|
||||||
|
const groupMembershipCreates = (
|
||||||
|
sql.match(/CREATE POLICY tenant_isolation_policy ON "GroupMembership"/g) ?? []
|
||||||
|
).length;
|
||||||
|
const moduleGrantCreates = (
|
||||||
|
sql.match(/CREATE POLICY tenant_isolation_policy ON "ModuleGrant"/g) ?? []
|
||||||
|
).length;
|
||||||
|
expect(groupMembershipCreates).toBe(1);
|
||||||
|
expect(moduleGrantCreates).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('die neue GroupMembership-Regel prueft die Benutzerseite UND (mit UND verknuepft) die Gruppenseite', () => {
|
||||||
|
const policy = extractPolicyBlock(sql, 'GroupMembership');
|
||||||
|
expect(policy).toContain('SELECT "id" FROM "Group" WHERE "tenantId" = current_tenant_id()');
|
||||||
|
expect(policy).toContain('SELECT "id" FROM "User" WHERE "tenantId" = current_tenant_id()');
|
||||||
|
expect(policy).toMatch(/AND\s+"userId"\s+IN/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('die neue ModuleGrant-Regel prueft die referenzierte Gruppe UND den referenzierten Benutzer, beide mit Leer-Zulassung (D-04)', () => {
|
||||||
|
const policy = extractPolicyBlock(sql, 'ModuleGrant');
|
||||||
|
expect(policy).toContain('"groupId" IS NULL');
|
||||||
|
expect(policy).toContain('"userId" IS NULL');
|
||||||
|
expect(policy).toContain('SELECT "id" FROM "Group" WHERE "tenantId" = current_tenant_id()');
|
||||||
|
expect(policy).toContain('SELECT "id" FROM "User" WHERE "tenantId" = current_tenant_id()');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loest die abgeloeste Regel auf TenderRssFeedSource ab und legt genau vier nach Befehl getrennte Regeln an', () => {
|
||||||
|
expect(sql).toContain('DROP POLICY tenant_isolation_policy ON "TenderRssFeedSource"');
|
||||||
|
for (const name of [
|
||||||
|
'tenant_platform_read_policy',
|
||||||
|
'tenant_insert_policy',
|
||||||
|
'tenant_update_policy',
|
||||||
|
'tenant_delete_policy',
|
||||||
|
]) {
|
||||||
|
expect(sql).toContain(`CREATE POLICY ${name} ON "TenderRssFeedSource"`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ausschliesslich die Leseregel auf TenderRssFeedSource laesst Zeilen ohne Mandant zu', () => {
|
||||||
|
const readPolicy = extractPolicyBlock(sql, 'TenderRssFeedSource', 'tenant_platform_read_policy');
|
||||||
|
const insertPolicy = extractPolicyBlock(sql, 'TenderRssFeedSource', 'tenant_insert_policy');
|
||||||
|
const updatePolicy = extractPolicyBlock(sql, 'TenderRssFeedSource', 'tenant_update_policy');
|
||||||
|
const deletePolicy = extractPolicyBlock(sql, 'TenderRssFeedSource', 'tenant_delete_policy');
|
||||||
|
|
||||||
|
expect(readPolicy).toContain('IS NULL');
|
||||||
|
for (const writePolicy of [insertPolicy, updatePolicy, deletePolicy]) {
|
||||||
|
expect(writePolicy).not.toContain('IS NULL');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fasst SearchProvider nicht an — kein DROP POLICY und kein CREATE POLICY fuer diese Tabelle', () => {
|
||||||
|
expect(sql).not.toMatch(/(DROP|CREATE) POLICY [\w ]*ON "SearchProvider"/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('add_group_internal_name_and_object_guid migration.sql (D-04)', () => {
|
describe('add_group_internal_name_and_object_guid migration.sql (D-04)', () => {
|
||||||
const sql = readMigrationSql('_add_group_internal_name_and_object_guid');
|
const sql = readMigrationSql('_add_group_internal_name_and_object_guid');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user