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:
@@ -389,6 +389,40 @@ function extractPolicySql(migrationSql, tableName) {
|
||||
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
|
||||
* 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
|
||||
* den vier Policies WORTGLEICH aus den beiden ausgelieferten Migrationen
|
||||
* (nicht im Werkzeug nachgetippt, vgl. runLdapAreaChecks). Findet die
|
||||
* Extraktion eine der vier nicht, meldet dieser Abschnitt eine
|
||||
* FEHLGESCHLAGENE Pruefung und bricht ab, statt mit einer geratenen Policy
|
||||
* weiterzumessen.
|
||||
* den vier Policies WORTGLEICH aus den ausgelieferten Migrationen (nicht im
|
||||
* Werkzeug nachgetippt, vgl. runLdapAreaChecks). Findet die Extraktion eine
|
||||
* der vier nicht, meldet dieser Abschnitt eine FEHLGESCHLAGENE Pruefung und
|
||||
* bricht ab, statt mit einer geratenen Policy 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
|
||||
* 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) {
|
||||
const groupsMigrationSql = readGroupsRlsPoliciesMigrationSql();
|
||||
const remainingMigrationSql = readRemainingTenantTablesMigrationSql();
|
||||
const widenMigrationSql = readRlsWidenMigrationSql();
|
||||
|
||||
const groupPolicy = groupsMigrationSql ? extractPolicySql(groupsMigrationSql, 'Group') : null;
|
||||
const groupMembershipPolicy = groupsMigrationSql
|
||||
? extractPolicySql(groupsMigrationSql, 'GroupMembership')
|
||||
const groupMembershipPolicy = widenMigrationSql
|
||||
? extractPolicySql(widenMigrationSql, 'GroupMembership')
|
||||
: null;
|
||||
const moduleGrantPolicy = groupsMigrationSql
|
||||
? extractPolicySql(groupsMigrationSql, 'ModuleGrant')
|
||||
const moduleGrantPolicy = widenMigrationSql
|
||||
? extractPolicySql(widenMigrationSql, 'ModuleGrant')
|
||||
: null;
|
||||
const tenantModuleActivationPolicy = remainingMigrationSql
|
||||
? extractPolicySql(remainingMigrationSql, 'TenantModuleActivation')
|
||||
@@ -602,7 +645,7 @@ async function runGroupsAreaChecks(adminUrl, scratchRoleUrl, results) {
|
||||
results,
|
||||
'groups-policies-aus-migration-gefunden',
|
||||
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;
|
||||
}
|
||||
@@ -735,31 +778,63 @@ async function runGroupsAreaChecks(adminUrl, scratchRoleUrl, results) {
|
||||
foreignGroupInsertDetail,
|
||||
);
|
||||
|
||||
// groupmembership-schreiben-fremder-benutzer-nicht-verhindert (Befund E):
|
||||
// das GELINGEN dieses INSERTs ist das bestandene Ergebnis — es belegt,
|
||||
// dass die Policy nur die Gruppenseite prueft, nicht die Benutzerseite.
|
||||
let foreignUserInsertSucceeded = false;
|
||||
// groupmembership-schreiben-fremder-benutzer-abgelehnt (260910-jab,
|
||||
// T-JTS-02) — Umkehr der bisherigen loch-behauptenden Pruefung
|
||||
// `groupmembership-schreiben-fremder-benutzer-nicht-verhindert`. Gemessen
|
||||
// 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 = '';
|
||||
try {
|
||||
await forTenantQuery(
|
||||
prisma,
|
||||
'TENANT-A',
|
||||
(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 =
|
||||
'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) {
|
||||
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(
|
||||
results,
|
||||
'groupmembership-schreiben-fremder-benutzer-nicht-verhindert',
|
||||
foreignUserInsertSucceeded,
|
||||
'groupmembership-schreiben-fremder-benutzer-abgelehnt',
|
||||
foreignUserInsertRejected,
|
||||
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
|
||||
const grantRowsForA = await forTenantQuery(prisma, 'TENANT-A', (tx) =>
|
||||
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))}`,
|
||||
);
|
||||
|
||||
// modulegrant-fremde-gruppe-trotz-eigener-mandantenkennung-erlaubt
|
||||
// (Befund F): auch hier ist das Durchgehen das bestandene Ergebnis.
|
||||
let foreignGroupGrantSucceeded = false;
|
||||
// modulegrant-fremde-gruppe-abgelehnt (260910-jab, T-JTS-03) — Umkehr
|
||||
// der bisherigen loch-behauptenden Pruefung
|
||||
// `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 = '';
|
||||
try {
|
||||
await forTenantQuery(
|
||||
prisma,
|
||||
'TENANT-A',
|
||||
(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 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';
|
||||
foreignGroupGrantDetail = 'INSERT mit korrekter eigener tenantId, aber fremder groupId ist NICHT fehlgeschlagen';
|
||||
} 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(
|
||||
results,
|
||||
'modulegrant-fremde-gruppe-trotz-eigener-mandantenkennung-erlaubt',
|
||||
foreignGroupGrantSucceeded,
|
||||
'modulegrant-fremde-gruppe-abgelehnt',
|
||||
foreignGroupGrantRejected,
|
||||
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
|
||||
const activationRowsForA = await forTenantQuery(prisma, 'TENANT-A', (tx) =>
|
||||
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
|
||||
* unter der Rolle ohne BYPASSRLS, mit den fuenf Policies WORTGLEICH aus der
|
||||
* ausgelieferten Migration `_rls_remaining_tenant_tables` (dieselbe Datei,
|
||||
* die bereits die TenantModuleActivation-Policy fuer runGroupsAreaChecks
|
||||
* liefert). Findet die Extraktion eine der fuenf nicht, meldet dieser
|
||||
* Abschnitt eine FEHLGESCHLAGENE Pruefung und bricht ab, statt mit einer
|
||||
* geratenen Policy weiterzumessen.
|
||||
* Aufgabe 1 (260909-laa/260910-jab) — misst die Sonderfaelle des Bereichs
|
||||
* tenders unter der Rolle ohne BYPASSRLS. Vier der fuenf Policies
|
||||
* (TenderEmailConfig, TenderNotificationPref, TenderSavedSearch,
|
||||
* TenderTriage) kommen WORTGLEICH aus der ausgelieferten Migration
|
||||
* `_rls_remaining_tenant_tables` (dieselbe Datei, die bereits die
|
||||
* TenantModuleActivation-Policy fuer runGroupsAreaChecks liefert).
|
||||
* 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
|
||||
* aufsetzt — anders als runGroupsAreaChecks() (Vorbedingung fuer
|
||||
@@ -829,6 +968,7 @@ async function runGroupsAreaChecks(adminUrl, scratchRoleUrl, results) {
|
||||
*/
|
||||
async function runTendersAreaChecks(adminUrl, scratchRoleUrl, results) {
|
||||
const remainingMigrationSql = readRemainingTenantTablesMigrationSql();
|
||||
const widenMigrationSql = readRlsWidenMigrationSql();
|
||||
|
||||
const emailConfigPolicy = remainingMigrationSql
|
||||
? extractPolicySql(remainingMigrationSql, 'TenderEmailConfig')
|
||||
@@ -836,9 +976,9 @@ async function runTendersAreaChecks(adminUrl, scratchRoleUrl, results) {
|
||||
const notificationPrefPolicy = remainingMigrationSql
|
||||
? extractPolicySql(remainingMigrationSql, 'TenderNotificationPref')
|
||||
: null;
|
||||
const rssFeedPolicy = remainingMigrationSql
|
||||
? extractPolicySql(remainingMigrationSql, 'TenderRssFeedSource')
|
||||
: null;
|
||||
const rssFeedPolicies = widenMigrationSql
|
||||
? extractAllPolicySql(widenMigrationSql, 'TenderRssFeedSource')
|
||||
: [];
|
||||
const savedSearchPolicy = remainingMigrationSql
|
||||
? extractPolicySql(remainingMigrationSql, 'TenderSavedSearch')
|
||||
: null;
|
||||
@@ -849,7 +989,7 @@ async function runTendersAreaChecks(adminUrl, scratchRoleUrl, results) {
|
||||
if (
|
||||
!emailConfigPolicy ||
|
||||
!notificationPrefPolicy ||
|
||||
!rssFeedPolicy ||
|
||||
rssFeedPolicies.length !== 4 ||
|
||||
!savedSearchPolicy ||
|
||||
!triagePolicy
|
||||
) {
|
||||
@@ -857,7 +997,7 @@ async function runTendersAreaChecks(adminUrl, scratchRoleUrl, results) {
|
||||
results,
|
||||
'tenders-policies-aus-migration-gefunden',
|
||||
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;
|
||||
}
|
||||
@@ -917,7 +1057,9 @@ async function runTendersAreaChecks(adminUrl, scratchRoleUrl, results) {
|
||||
}
|
||||
await db.$executeRawUnsafe(emailConfigPolicy);
|
||||
await db.$executeRawUnsafe(notificationPrefPolicy);
|
||||
await db.$executeRawUnsafe(rssFeedPolicy);
|
||||
for (const policySql of rssFeedPolicies) {
|
||||
await db.$executeRawUnsafe(policySql);
|
||||
}
|
||||
await db.$executeRawUnsafe(savedSearchPolicy);
|
||||
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))}`,
|
||||
);
|
||||
|
||||
// tenderrssfeed-plattformzeile-unter-jedem-mandanten-unsichtbar
|
||||
// (WINDOWS #19): die plattformweite Zeile (userId/tenantId NULL) ist
|
||||
// unter BEIDEN Mandantenkontexten unsichtbar, weil die ausgelieferte
|
||||
// Policy "tenantId" = current_tenant_id() NULL nie gleich vergleicht.
|
||||
// Bestanden, wenn sie unter beiden Kontexten fehlt — die Folge: die
|
||||
// drei RSS-Pfade, die plattformweite Zeilen beruehren, duerfen nicht
|
||||
// gebunden werden, solange die Policy-Semantik unveraendert ist.
|
||||
// tenderrssfeed-plattformzeile-gebunden-sichtbar (260910-jab, WINDOWS
|
||||
// #19) — Umkehr von 'tenderrssfeed-plattformzeile-unter-jedem-
|
||||
// mandanten-unsichtbar': die plattformweite Zeile (userId/tenantId
|
||||
// NULL) ist jetzt unter BEIDEN Mandantenkontexten SICHTBAR, weil die
|
||||
// Leseregel der neuen vier Policies (tenant_platform_read_policy,
|
||||
// 20260910120000_rls_widen_membership_grant_and_platform_read) Zeilen
|
||||
// ohne Mandant ausdruecklich einschliesst.
|
||||
const rssRowsForA = await forTenantQuery(prisma, 'TENANT-A', (tx) =>
|
||||
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');
|
||||
report(
|
||||
results,
|
||||
'tenderrssfeed-plattformzeile-unter-jedem-mandanten-unsichtbar',
|
||||
!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`,
|
||||
'tenderrssfeed-plattformzeile-gebunden-sichtbar',
|
||||
platformRowVisibleForA && platformRowVisibleForB,
|
||||
`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
|
||||
// gebundenes INSERT unter TENANT-A mit tenantId=NULL wird abgewiesen —
|
||||
// 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 platformInsertDetail = '';
|
||||
try {
|
||||
@@ -1076,7 +1250,7 @@ async function runTendersAreaChecks(adminUrl, scratchRoleUrl, results) {
|
||||
platformInsertDetail = 'gebundenes INSERT mit tenantId=NULL ist NICHT fehlgeschlagen';
|
||||
} catch (err) {
|
||||
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(
|
||||
results,
|
||||
@@ -1085,6 +1259,75 @@ async function runTendersAreaChecks(adminUrl, scratchRoleUrl, results) {
|
||||
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
|
||||
// (Befund F): unter TENANT-A ein INSERT fuer ein Paar (userId,
|
||||
// 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
|
||||
* 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))}`,
|
||||
);
|
||||
|
||||
// 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');
|
||||
report(
|
||||
results,
|
||||
'gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus',
|
||||
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 —
|
||||
@@ -2296,6 +2624,7 @@ async function main() {
|
||||
await runLdapAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||
await runGroupsAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||
await runTendersAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||
await runSearchProviderAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||
await runDkvAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||
await runUserAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||
await runModuleRegistryAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||
|
||||
Reference in New Issue
Block a user