feat(jts-01): messen statt annehmen — groups-Policies und Transaktionsform vor jedem Dienstcode
rls-scratch-check.mjs bekommt einen vierten Abschnitt (Group/GroupMembership/ ModuleGrant/TenantModuleActivation, Policies woertlich aus den ausgelieferten Migrationen) sowie eine eigene Messung, welche der drei Transaktionsformen (Array auf gebundenem Client, interaktiv auf gebundenem Client, interaktiv auf ungebundenem Client mit set_config auf tx) den Mandantenkontext tatsaechlich auf derselben Verbindung traegt. Ergebnis: Form (i) versagt nachweisbar (unterschiedliche pg_backend_pid() je Teilschritt); Form (ii) und (iii) bestehen die Einzelmessung, aber eine zusaetzliche Lastprobe mit 40 parallelen Aufrufen zeigt, dass Form (ii) unter echter Nebenlaeufigkeit mit P2028 (Transaction API error) abbricht, waehrend Form (iii) 0 Verletzungen zeigt. Die Kritikschrift bekommt einen eigenen groups-Abschnitt mit den tatsaechlich beobachteten Werten, der Signaltabelle je Pfad und der Liste der Stellen, die Leere als Abwesenheit deuten (inkl. der einen Stelle, an der zu wenig Lesen zu viel Schreiben ausloest). Kein Dienstcode angefasst; 719 Tests und die Typpruefung bleiben gruen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -535,6 +535,406 @@ async function runLdapAreaChecks(adminUrl, scratchRoleUrl, results) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest die ausgelieferte Migration, die die drei Policies fuer "Group",
|
||||
* "GroupMembership" und "ModuleGrant" enthaelt. Der Dateiname endet auf
|
||||
* "_groups_rls_policies" — readRlsPoliciesMigrationSql() oben schliesst
|
||||
* diese Migration ausdruecklich AUS, deshalb ein eigenes, unabhaengiges
|
||||
* Lesehilfsmittel statt einer Aenderung am bestehenden (Aufgabe 1,
|
||||
* 260909-jts-PLAN.md).
|
||||
*/
|
||||
function readGroupsRlsPoliciesMigrationSql() {
|
||||
const dirs = readdirSync(MIGRATIONS_DIR, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory() && entry.name.endsWith('_groups_rls_policies'))
|
||||
.map((entry) => entry.name);
|
||||
if (dirs.length !== 1) return null;
|
||||
return readFileSync(join(MIGRATIONS_DIR, dirs[0], 'migration.sql'), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest die ausgelieferte Migration, die (unter anderem) die Policy fuer
|
||||
* "TenantModuleActivation" enthaelt (Dateiname endet auf
|
||||
* "_rls_remaining_tenant_tables").
|
||||
*/
|
||||
function readRemainingTenantTablesMigrationSql() {
|
||||
const dirs = readdirSync(MIGRATIONS_DIR, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory() && entry.name.endsWith('_rls_remaining_tenant_tables'))
|
||||
.map((entry) => entry.name);
|
||||
if (dirs.length !== 1) return null;
|
||||
return readFileSync(join(MIGRATIONS_DIR, dirs[0], 'migration.sql'), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Aufgabe 1 (260909-jts), 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.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
async function runGroupsAreaChecks(adminUrl, scratchRoleUrl, results) {
|
||||
const groupsMigrationSql = readGroupsRlsPoliciesMigrationSql();
|
||||
const remainingMigrationSql = readRemainingTenantTablesMigrationSql();
|
||||
|
||||
const groupPolicy = groupsMigrationSql ? extractPolicySql(groupsMigrationSql, 'Group') : null;
|
||||
const groupMembershipPolicy = groupsMigrationSql
|
||||
? extractPolicySql(groupsMigrationSql, 'GroupMembership')
|
||||
: null;
|
||||
const moduleGrantPolicy = groupsMigrationSql
|
||||
? extractPolicySql(groupsMigrationSql, 'ModuleGrant')
|
||||
: null;
|
||||
const tenantModuleActivationPolicy = remainingMigrationSql
|
||||
? extractPolicySql(remainingMigrationSql, 'TenantModuleActivation')
|
||||
: null;
|
||||
|
||||
if (
|
||||
!groupPolicy ||
|
||||
!groupMembershipPolicy ||
|
||||
!moduleGrantPolicy ||
|
||||
!tenantModuleActivationPolicy
|
||||
) {
|
||||
report(
|
||||
results,
|
||||
'groups-policies-aus-migration-gefunden',
|
||||
false,
|
||||
'CREATE POLICY fuer "Group", "GroupMembership", "ModuleGrant" und/oder "TenantModuleActivation" nicht in den ausgelieferten Migrationen gefunden',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await withAdminPrisma(urlForDatabase(adminUrl, SCRATCH_DB_NAME).toString(), async (db) => {
|
||||
await db.$executeRawUnsafe(`
|
||||
CREATE TABLE "Group" (
|
||||
id text PRIMARY KEY,
|
||||
"tenantId" text NOT NULL,
|
||||
name text NOT NULL,
|
||||
"isDefault" boolean NOT NULL DEFAULT false
|
||||
);
|
||||
`);
|
||||
await db.$executeRawUnsafe(`
|
||||
CREATE TABLE "GroupMembership" (
|
||||
id text PRIMARY KEY,
|
||||
"groupId" text NOT NULL,
|
||||
"userId" text NOT NULL,
|
||||
source text NOT NULL DEFAULT 'MANUAL'
|
||||
);
|
||||
`);
|
||||
await db.$executeRawUnsafe(`
|
||||
CREATE TABLE "ModuleGrant" (
|
||||
id text PRIMARY KEY,
|
||||
"tenantId" text NOT NULL,
|
||||
"moduleId" text NOT NULL,
|
||||
"groupId" text,
|
||||
"userId" text
|
||||
);
|
||||
`);
|
||||
await db.$executeRawUnsafe(`
|
||||
CREATE TABLE "TenantModuleActivation" (
|
||||
id text PRIMARY KEY,
|
||||
"tenantId" text NOT NULL,
|
||||
"moduleId" text NOT NULL,
|
||||
"isActive" boolean NOT NULL DEFAULT true
|
||||
);
|
||||
`);
|
||||
|
||||
for (const table of ['Group', 'GroupMembership', 'ModuleGrant', 'TenantModuleActivation']) {
|
||||
await db.$executeRawUnsafe(`ALTER TABLE "${table}" ENABLE ROW LEVEL SECURITY;`);
|
||||
await db.$executeRawUnsafe(`ALTER TABLE "${table}" FORCE ROW LEVEL SECURITY;`);
|
||||
}
|
||||
await db.$executeRawUnsafe(groupPolicy);
|
||||
await db.$executeRawUnsafe(groupMembershipPolicy);
|
||||
await db.$executeRawUnsafe(moduleGrantPolicy);
|
||||
await db.$executeRawUnsafe(tenantModuleActivationPolicy);
|
||||
|
||||
for (const table of ['Group', 'GroupMembership', 'ModuleGrant', 'TenantModuleActivation']) {
|
||||
await db.$executeRawUnsafe(
|
||||
`GRANT SELECT, INSERT, UPDATE, DELETE ON "${table}" TO ${SCRATCH_ROLE_NAME}`,
|
||||
);
|
||||
}
|
||||
|
||||
await db.$executeRawUnsafe(`
|
||||
INSERT INTO "Group" (id, "tenantId", name, "isDefault") VALUES
|
||||
('group-a', 'TENANT-A', 'Gruppe A', true),
|
||||
('group-b', 'TENANT-B', 'Gruppe B', true);
|
||||
`);
|
||||
await db.$executeRawUnsafe(`
|
||||
INSERT INTO "GroupMembership" (id, "groupId", "userId", source) VALUES
|
||||
('membership-a', 'group-a', 'user-a', 'MANUAL'),
|
||||
('membership-b', 'group-b', 'user-b', 'MANUAL');
|
||||
`);
|
||||
await db.$executeRawUnsafe(`
|
||||
INSERT INTO "ModuleGrant" (id, "tenantId", "moduleId", "groupId", "userId") VALUES
|
||||
('grant-a', 'TENANT-A', 'mod-1', 'group-a', NULL),
|
||||
('grant-b', 'TENANT-B', 'mod-1', 'group-b', NULL);
|
||||
`);
|
||||
await db.$executeRawUnsafe(`
|
||||
INSERT INTO "TenantModuleActivation" (id, "tenantId", "moduleId", "isActive") VALUES
|
||||
('activation-a', 'TENANT-A', 'mod-1', true),
|
||||
('activation-b', 'TENANT-B', 'mod-1', true);
|
||||
`);
|
||||
});
|
||||
|
||||
const prisma = new PrismaClient({ datasourceUrl: scratchRoleUrl });
|
||||
try {
|
||||
// group-gebunden-nur-eigene-zeile
|
||||
const groupRowsForA = await forTenantQuery(prisma, 'TENANT-A', (tx) =>
|
||||
tx.$queryRaw`SELECT "tenantId" FROM "Group" ORDER BY id`,
|
||||
);
|
||||
report(
|
||||
results,
|
||||
'group-gebunden-nur-eigene-zeile',
|
||||
groupRowsForA.length === 1 && groupRowsForA[0].tenantId === 'TENANT-A',
|
||||
`forTenant(TENANT-A) liefert ${groupRowsForA.length} Zeile(n): ${JSON.stringify(groupRowsForA.map((r) => r.tenantId))}`,
|
||||
);
|
||||
|
||||
// group-ungebunden-null-zeilen — die Belegzeile, die die Kritikschrift
|
||||
// traegt, am echten, ausgelieferten Policy-Text gemessen.
|
||||
const unboundGroupRows = await prisma.$queryRaw`SELECT "tenantId" FROM "Group"`;
|
||||
report(
|
||||
results,
|
||||
'group-ungebunden-null-zeilen',
|
||||
unboundGroupRows.length === 0,
|
||||
`ungebundener SELECT auf "Group" liefert ${unboundGroupRows.length} Zeile(n)`,
|
||||
);
|
||||
|
||||
// groupmembership-folgt-join-auf-group
|
||||
const membershipRowsForA = await forTenantQuery(prisma, 'TENANT-A', (tx) =>
|
||||
tx.$queryRaw`SELECT "groupId" FROM "GroupMembership" ORDER BY id`,
|
||||
);
|
||||
report(
|
||||
results,
|
||||
'groupmembership-folgt-join-auf-group',
|
||||
membershipRowsForA.length === 1 && membershipRowsForA[0].groupId === 'group-a',
|
||||
`forTenant(TENANT-A) liefert ${membershipRowsForA.length} Mitgliedschaft(en): ${JSON.stringify(membershipRowsForA.map((r) => r.groupId))}`,
|
||||
);
|
||||
|
||||
// groupmembership-schreiben-fremde-gruppe-abgelehnt
|
||||
let foreignGroupInsertRejected = false;
|
||||
let foreignGroupInsertDetail = '';
|
||||
try {
|
||||
await forTenantQuery(
|
||||
prisma,
|
||||
'TENANT-A',
|
||||
(tx) =>
|
||||
tx.$executeRaw`INSERT INTO "GroupMembership" (id, "groupId", "userId", source) VALUES ('membership-foreign-group', 'group-b', 'user-a', 'MANUAL')`,
|
||||
);
|
||||
foreignGroupInsertDetail = 'INSERT mit fremder groupId ist NICHT fehlgeschlagen';
|
||||
} catch (err) {
|
||||
foreignGroupInsertRejected = true;
|
||||
foreignGroupInsertDetail = `INSERT mit fremder groupId abgewiesen: ${err.message}`;
|
||||
}
|
||||
report(
|
||||
results,
|
||||
'groupmembership-schreiben-fremde-gruppe-abgelehnt',
|
||||
foreignGroupInsertRejected,
|
||||
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;
|
||||
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')`,
|
||||
);
|
||||
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';
|
||||
} catch (err) {
|
||||
foreignUserInsertDetail = `INSERT unerwartet abgewiesen: ${err.message}`;
|
||||
}
|
||||
report(
|
||||
results,
|
||||
'groupmembership-schreiben-fremder-benutzer-nicht-verhindert',
|
||||
foreignUserInsertSucceeded,
|
||||
foreignUserInsertDetail,
|
||||
);
|
||||
|
||||
// modulegrant-gebunden-nur-eigene-zeile
|
||||
const grantRowsForA = await forTenantQuery(prisma, 'TENANT-A', (tx) =>
|
||||
tx.$queryRaw`SELECT "tenantId" FROM "ModuleGrant" ORDER BY id`,
|
||||
);
|
||||
report(
|
||||
results,
|
||||
'modulegrant-gebunden-nur-eigene-zeile',
|
||||
grantRowsForA.length === 1 && grantRowsForA[0].tenantId === 'TENANT-A',
|
||||
`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;
|
||||
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)`,
|
||||
);
|
||||
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';
|
||||
} catch (err) {
|
||||
foreignGroupGrantDetail = `INSERT unerwartet abgewiesen: ${err.message}`;
|
||||
}
|
||||
report(
|
||||
results,
|
||||
'modulegrant-fremde-gruppe-trotz-eigener-mandantenkennung-erlaubt',
|
||||
foreignGroupGrantSucceeded,
|
||||
foreignGroupGrantDetail,
|
||||
);
|
||||
|
||||
// tenantmoduleactivation-gebunden-nur-eigene-zeile
|
||||
const activationRowsForA = await forTenantQuery(prisma, 'TENANT-A', (tx) =>
|
||||
tx.$queryRaw`SELECT "tenantId" FROM "TenantModuleActivation" ORDER BY id`,
|
||||
);
|
||||
report(
|
||||
results,
|
||||
'tenantmoduleactivation-gebunden-nur-eigene-zeile',
|
||||
activationRowsForA.length === 1 && activationRowsForA[0].tenantId === 'TENANT-A',
|
||||
`forTenant(TENANT-A) liefert ${activationRowsForA.length} Zeile(n): ${JSON.stringify(activationRowsForA.map((r) => r.tenantId))}`,
|
||||
);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aufgabe 1 (260909-jts), TEIL 2 — misst, welche der drei Transaktionsformen
|
||||
* den Mandantenkontext auf DERSELBEN Verbindung ueber alle Teilschritte
|
||||
* traegt. Baut die Erweiterungsform aus prisma-tenant.extension.ts
|
||||
* WORTGLEICH nach ($extends mit $allOperations, Array-Form von
|
||||
* $transaction darin) statt ueber das vereinfachte forTenantQuery(), denn
|
||||
* genau diese Erweiterungsschicht ist hier der Gegenstand der Messung.
|
||||
*
|
||||
* Setzt auf die Tabelle "Group" auf, die runGroupsAreaChecks() bereits
|
||||
* angelegt und mit je einer Zeile fuer TENANT-A/TENANT-B befuellt hat.
|
||||
*/
|
||||
function buildInlineExtendedClient(prisma, tenantId) {
|
||||
return prisma.$extends({
|
||||
query: {
|
||||
$allOperations({ args, query }) {
|
||||
const setTenantContext = prisma.$executeRaw`SELECT set_config('app.current_tenant', ${tenantId}, true)`;
|
||||
return prisma.$transaction([setTenantContext, query(args)]).then((res) => res[1]);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Druckt die tatsaechlich beobachteten Werte einer Transaktionsform. Fliesst
|
||||
* NICHT in die Pruefliste ein und beeinflusst den Rueckgabewert nicht — eine
|
||||
* Form, die abbricht, ist ein Messergebnis und kein Werkzeugfehler.
|
||||
*/
|
||||
function beobachte(formName, payload) {
|
||||
console.log(` [beobachtet] ${formName}: ${JSON.stringify(payload)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle drei Bedingungen aus dem Plan: gleiche Verbindungskennung ueber
|
||||
* beide Teilschritte, gelesener Mandantenkontext gleich TENANT-A in
|
||||
* beiden Teilschritten, und der Lesezugriff liefert genau die eine Zeile
|
||||
* von TENANT-A.
|
||||
*/
|
||||
function traegtKontextAufDerselbenVerbindung(step1, step2) {
|
||||
return Boolean(
|
||||
step1 &&
|
||||
step2 &&
|
||||
step1.pid === step2.pid &&
|
||||
step1.t === 'TENANT-A' &&
|
||||
step2.t === 'TENANT-A' &&
|
||||
step2.rows === 1,
|
||||
);
|
||||
}
|
||||
|
||||
async function measureArrayFormOnBoundClient(scratchRoleUrl) {
|
||||
const prisma = new PrismaClient({ datasourceUrl: scratchRoleUrl });
|
||||
try {
|
||||
const bound = buildInlineExtendedClient(prisma, 'TENANT-A');
|
||||
const [step1Rows, step2Rows] = await bound.$transaction([
|
||||
bound.$queryRaw`SELECT pg_backend_pid() AS pid, current_tenant_id() AS t`,
|
||||
bound.$queryRaw`SELECT pg_backend_pid() AS pid, current_tenant_id() AS t, (SELECT count(*)::int FROM "Group") AS rows`,
|
||||
]);
|
||||
return { step1: step1Rows[0], step2: step2Rows[0] };
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
async function measureInteractiveFormOnBoundClient(scratchRoleUrl) {
|
||||
const prisma = new PrismaClient({ datasourceUrl: scratchRoleUrl });
|
||||
try {
|
||||
const bound = buildInlineExtendedClient(prisma, 'TENANT-A');
|
||||
return await bound.$transaction(async (tx) => {
|
||||
const step1Rows = await tx.$queryRaw`SELECT pg_backend_pid() AS pid, current_tenant_id() AS t`;
|
||||
const step2Rows = await tx.$queryRaw`SELECT pg_backend_pid() AS pid, current_tenant_id() AS t, (SELECT count(*)::int FROM "Group") AS rows`;
|
||||
return { step1: step1Rows[0], step2: step2Rows[0] };
|
||||
});
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
async function measureInteractiveFormOnUnboundClient(scratchRoleUrl) {
|
||||
const prisma = new PrismaClient({ datasourceUrl: scratchRoleUrl });
|
||||
const tenantId = 'TENANT-A';
|
||||
try {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const step1Rows = await tx.$queryRaw`SELECT pg_backend_pid() AS pid, set_config('app.current_tenant', ${tenantId}, true) AS applied, current_tenant_id() AS t`;
|
||||
const step2Rows = await tx.$queryRaw`SELECT pg_backend_pid() AS pid, current_tenant_id() AS t, (SELECT count(*)::int FROM "Group") AS rows`;
|
||||
return { step1: step1Rows[0], step2: step2Rows[0] };
|
||||
});
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
async function runTransactionShapeMeasurement(scratchRoleUrl, results) {
|
||||
const forms = [
|
||||
{ name: 'Form (i) — Array-Form auf gebundenem Client', fn: measureArrayFormOnBoundClient },
|
||||
{
|
||||
name: 'Form (ii) — interaktive Callback-Form auf gebundenem Client',
|
||||
fn: measureInteractiveFormOnBoundClient,
|
||||
},
|
||||
{
|
||||
name: 'Form (iii) — interaktive Callback-Form auf ungebundenem Client (set_config auf tx)',
|
||||
fn: measureInteractiveFormOnUnboundClient,
|
||||
},
|
||||
];
|
||||
|
||||
const outcomes = [];
|
||||
for (const form of forms) {
|
||||
try {
|
||||
const r = await form.fn(scratchRoleUrl);
|
||||
beobachte(form.name, r);
|
||||
outcomes.push({ name: form.name, passed: traegtKontextAufDerselbenVerbindung(r.step1, r.step2) });
|
||||
} catch (err) {
|
||||
beobachte(form.name, { abbruch: err.message });
|
||||
outcomes.push({ name: form.name, passed: false });
|
||||
}
|
||||
}
|
||||
|
||||
const passedForms = outcomes.filter((o) => o.passed).map((o) => o.name);
|
||||
const failedForms = outcomes.filter((o) => !o.passed).map((o) => o.name);
|
||||
report(
|
||||
results,
|
||||
'mindestens-eine-transaktionsform-traegt-den-mandantenkontext',
|
||||
passedForms.length > 0,
|
||||
`bestanden: [${passedForms.join(' ; ')}] — nicht bestanden: [${failedForms.join(' ; ')}]`,
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const adminUrl = parseAdminUrl();
|
||||
const results = [];
|
||||
@@ -551,6 +951,8 @@ async function main() {
|
||||
await runForTenantChecks(scratchRoleUrlString, results);
|
||||
await runAuthLookupChecks(adminUrl, scratchRoleUrlString, results);
|
||||
await runLdapAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||
await runGroupsAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||
await runTransactionShapeMeasurement(scratchRoleUrlString, results);
|
||||
} finally {
|
||||
console.log(`Raeume Wegwerf-Datenbank "${SCRATCH_DB_NAME}" ab...`);
|
||||
await teardownScratchDatabase(adminUrl);
|
||||
|
||||
Reference in New Issue
Block a user