M8: Mandanten-Isolation und Rollenmatrix automatisiert testen
scripts/check-m8-tenant-isolation.php: legt zwei frische, isolierte Test-Mandanten mit je einem Teilnehmer, einer Ledger-Buchung, einem Hinweis und einem Audit-Log-Eintrag an und prueft 10 Faelle - Lesezugriffe (Teilnehmerlisten, Einzelabruf, letzte Buchungen, aktive Hinweise, Audit-Log) und Schreibzugriffe (Buchung, Zugangsvergabe, Storno) sind strikt auf den jeweils richtigen Mandanten beschraenkt. Raeumt sich selbst auf. 10/10 gruen. scripts/check-m8-role-matrix.php: legt einen Test-Mandanten mit je einem Nutzer pro Rolle an (owner/admin/treasurer/member/viewer), loggt sich per echtem HTTP-Request ein (manueller Cookie-Jar ueber file_get_contents, da diese PHP-Installation keine curl-Extension hat) und prueft alle elf rollen-geschuetzten Seiten gegen die erwartete Rollenliste. 55/55 gruen (5 Rollen x 11 Seiten) - bestaetigt, dass die Rollenpruefungen ueberall konsistent mit dem im Plan dokumentierten Rollenmodell durchgesetzt sind. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+35
-2
@@ -91,18 +91,51 @@ mailversenden.php
|
||||
eine Einstellungsänderung im UI-Protokoll mit korrektem Nutzernamen
|
||||
erscheint. Testdaten anschließend entfernt.
|
||||
|
||||
## Mandanten-Isolation (automatisiert)
|
||||
|
||||
Umgesetzte Datei: `scripts/check-m8-tenant-isolation.php`
|
||||
|
||||
Legt zwei frische, isolierte Test-Mandanten mit je einem Teilnehmer, einer
|
||||
Ledger-Buchung, einem Hinweis und einem Audit-Log-Eintrag an und prüft
|
||||
10 Fälle: Teilnehmerlisten, Einzelabruf, letzte Buchungen, aktive Hinweise,
|
||||
Schreibversuche (Buchung, Zugangsvergabe, Storno) und Audit-Log sind
|
||||
strikt auf den jeweils richtigen Mandanten beschränkt; ein Zugriff über den
|
||||
falschen Mandanten liefert nichts beziehungsweise schlägt sauber fehl statt
|
||||
fremde Daten zurückzugeben. Räumt die Testdaten am Ende selbst auf.
|
||||
|
||||
Ergebnis: grün mit 10 Assertions.
|
||||
|
||||
## Rollenmatrix (automatisiert)
|
||||
|
||||
Umgesetzte Datei: `scripts/check-m8-role-matrix.php`
|
||||
|
||||
Legt einen Test-Mandanten mit je einem Nutzer pro Rolle (`owner`, `admin`,
|
||||
`treasurer`, `member`, `viewer`) an, loggt sich für jede Rolle per echtem
|
||||
HTTP-Request ein (manueller Cookie-Jar über `file_get_contents`, da diese
|
||||
PHP-Installation keine curl-Extension hat) und ruft alle rollen-geschützten
|
||||
Seiten auf: `kaffeeliste.php`, `mitarbeiterverwalten.php`, `hinweise.php`,
|
||||
`stricheintragen.php`, `einzahlung.php`, `letzteneintraege.php`,
|
||||
`csvupload.php`, `exportKaffeeliste.php`, `mailversenden.php`,
|
||||
`jahresauswertung.php`, `mandant-einstellungen.php`. Für jede
|
||||
Rolle-Seite-Kombination wird geprüft, ob der tatsächliche Zugriff (anhand
|
||||
der „Kein Zugriff"/„keine Berechtigung"-Marker in der Antwort) mit der
|
||||
erwarteten Rollenliste übereinstimmt.
|
||||
|
||||
Ergebnis: grün mit 55 Assertions (5 Rollen × 11 Seiten). Räumt die
|
||||
Testdaten am Ende selbst auf.
|
||||
|
||||
## Prüfstatus
|
||||
|
||||
- Golden Master: grün mit 104 Assertions.
|
||||
- M4 Ledger-Migration: grün mit 73 Assertions.
|
||||
- M3 Settings-Flow: grün mit 13 Assertions.
|
||||
- HTTP-Smoke: grün mit 26 geprüften Seiten.
|
||||
- M8 Mandanten-Isolation: grün mit 10 Assertions.
|
||||
- M8 Rollenmatrix: grün mit 55 Assertions.
|
||||
|
||||
## Noch offen in M8
|
||||
|
||||
- Content-Security-Policy (braucht Template-Bereinigung der Inline-Styles).
|
||||
- Mandanten-Isolation automatisiert testen.
|
||||
- Rollenmatrix automatisiert testen.
|
||||
- Datenexport pro Mandant.
|
||||
- Lösch-/Anonymisierungsprozess für Teilnehmer und Kunden.
|
||||
- Backup-/Restore-Prozess und Monitoring/Fehlerlogging dokumentieren.
|
||||
|
||||
@@ -308,7 +308,7 @@ Nicht tun:
|
||||
| M5 | App-Kern | Weit fortgeschritten: Kernseiten lesen und schreiben tenant-sicher gegen das Ledger, inklusive Hinweise und rollenbasiertem Zugang; offen ist ein eigener Zahlungs-Screen |
|
||||
| M6 | Betriebsflows | Abgeschlossen: Import, Export, Mail und Jahresabschluss sind auditierbar |
|
||||
| M7 | Landingpage | Public-Seite und Auth-Seiten sind im gemeinsamen Stil nutzbar; spätere Ausbaustufen folgen |
|
||||
| M8 | Härtung | Gestartet: Security-Headers, Rate-Limits und Audit-Log stehen; Isolation/Rollenmatrix-Tests, Datenexport, Löschung und Betrieb offen |
|
||||
| M8 | Härtung | Weit fortgeschritten: Security-Headers, Rate-Limits, Audit-Log, Mandanten-Isolations- und Rollenmatrix-Tests stehen; Datenexport, Löschung und Betriebs-Doku offen |
|
||||
| M9 | Cutover | Produktivumstellung ist vorbereitet und Legacy ist read-only |
|
||||
|
||||
### M0: Planungs- und Sicherheitsbaseline
|
||||
@@ -652,8 +652,13 @@ Schritte:
|
||||
- Rate-Limits und Security Headers: erledigt. Globale Security-Headers
|
||||
über `app/bootstrap.php` (ohne CSP, siehe `docs/m8-haertung.md`),
|
||||
DB-gestützte Rate-Limits für Login, Registrierung und Passwort-Reset.
|
||||
- Mandanten-Isolation testen.
|
||||
- Rollenmatrix testen.
|
||||
- Mandanten-Isolation testen: erledigt.
|
||||
`scripts/check-m8-tenant-isolation.php` legt zwei isolierte Test-Mandanten
|
||||
an und prüft Lese- und Schreibzugriffe über den jeweils falschen
|
||||
Mandanten (10 Assertions, grün).
|
||||
- Rollenmatrix testen: erledigt.
|
||||
`scripts/check-m8-role-matrix.php` prüft alle fünf Rollen gegen alle
|
||||
rollen-geschützten Seiten per echtem HTTP-Login (55 Assertions, grün).
|
||||
|
||||
Ergebnis:
|
||||
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/dev-db.php';
|
||||
|
||||
$baseUrl = rtrim((string)(getenv('SMOKE_BASE_URL') ?: 'http://127.0.0.1:8080'), '/');
|
||||
$pdo = dev_pdo();
|
||||
$suffix = bin2hex(random_bytes(4));
|
||||
$password = 'RoleMatrixTest123!';
|
||||
$roles = ['owner', 'admin', 'treasurer', 'member', 'viewer'];
|
||||
|
||||
// Seite => Rollen, die Zugriff haben sollen. Alle anderen Rollen muessen
|
||||
// abgewiesen werden. Muss mit den saas_user_has_role()-Aufrufen in den
|
||||
// jeweiligen Dateien uebereinstimmen.
|
||||
$pages = [
|
||||
'kaffeeliste.php' => ['owner', 'admin', 'treasurer'],
|
||||
'mitarbeiterverwalten.php' => ['owner', 'admin'],
|
||||
'hinweise.php' => ['owner', 'admin'],
|
||||
'stricheintragen.php' => ['owner', 'admin', 'treasurer'],
|
||||
'einzahlung.php' => ['owner', 'admin', 'treasurer'],
|
||||
'letzteneintraege.php' => ['owner', 'admin', 'treasurer'],
|
||||
'csvupload.php' => ['owner', 'admin', 'treasurer'],
|
||||
'exportKaffeeliste.php' => ['owner', 'admin', 'treasurer'],
|
||||
'mailversenden.php' => ['owner', 'admin', 'treasurer'],
|
||||
'jahresauswertung.php' => ['owner', 'admin', 'treasurer'],
|
||||
'mandant-einstellungen.php' => ['owner', 'admin'],
|
||||
];
|
||||
|
||||
$denialMarkers = ['kein zugriff', 'keine zugang', 'keine berechtigung'];
|
||||
|
||||
function m8rm_looks_denied(string $body): bool
|
||||
{
|
||||
global $denialMarkers;
|
||||
$lower = strtolower($body);
|
||||
foreach ($denialMarkers as $marker) {
|
||||
if (str_contains($lower, $marker)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimaler manueller Cookie-Jar auf Basis von file_get_contents, da diese
|
||||
* PHP-Installation keine curl-Extension hat. $cookies wird per Referenz
|
||||
* aktualisiert, sobald die Antwort Set-Cookie-Header enthaelt (insbesondere
|
||||
* nach dem Login, der die Session-ID per session_regenerate_id() erneuert).
|
||||
*
|
||||
* @param array<string,string> $cookies
|
||||
* @return array{status:int, body:string}
|
||||
*/
|
||||
function m8rm_request(string $url, array &$cookies, string $method = 'GET', ?string $postBody = null): array
|
||||
{
|
||||
$cookieHeader = '';
|
||||
if ($cookies !== []) {
|
||||
$pairs = [];
|
||||
foreach ($cookies as $name => $value) {
|
||||
$pairs[] = "{$name}={$value}";
|
||||
}
|
||||
$cookieHeader = 'Cookie: ' . implode('; ', $pairs) . "\r\n";
|
||||
}
|
||||
|
||||
$headers = "User-Agent: KaffeelisteRoleMatrix/1.0\r\n" . $cookieHeader;
|
||||
if ($postBody !== null) {
|
||||
$headers .= "Content-Type: application/x-www-form-urlencoded\r\n";
|
||||
}
|
||||
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => $method,
|
||||
'header' => $headers,
|
||||
'content' => $postBody,
|
||||
'timeout' => 15,
|
||||
'ignore_errors' => true,
|
||||
'follow_location' => 0,
|
||||
],
|
||||
]);
|
||||
|
||||
$body = @file_get_contents($url, false, $context);
|
||||
$responseHeaders = $http_response_header ?? [];
|
||||
$status = 0;
|
||||
|
||||
foreach ($responseHeaders as $header) {
|
||||
if (preg_match('~^HTTP/\S+\s+(\d{3})~', $header, $m) === 1) {
|
||||
$status = (int)$m[1];
|
||||
}
|
||||
if (preg_match('/^Set-Cookie:\s*([^=;]+)=([^;]+)/i', $header, $m) === 1) {
|
||||
$cookies[$m[1]] = $m[2];
|
||||
}
|
||||
}
|
||||
|
||||
return ['status' => $status, 'body' => (string)$body];
|
||||
}
|
||||
|
||||
$tenantId = null;
|
||||
$userIds = [];
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare('INSERT INTO tenants (slug, name, status) VALUES (?, ?, ?)');
|
||||
$stmt->execute(["rolematrix-{$suffix}", 'Rollenmatrix Tenant', 'active']);
|
||||
$tenantId = (int)$pdo->lastInsertId();
|
||||
$pdo->prepare('INSERT INTO tenant_settings (tenant_id) VALUES (?)')->execute([$tenantId]);
|
||||
|
||||
foreach ($roles as $role) {
|
||||
$email = "rolematrix-{$role}-{$suffix}@test.local";
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO users (email, email_norm, display_name, password_hash, status) VALUES (?, ?, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([$email, $email, "Rolematrix {$role}", password_hash($password, PASSWORD_DEFAULT), 'active']);
|
||||
$userId = (int)$pdo->lastInsertId();
|
||||
$userIds[$role] = $userId;
|
||||
|
||||
$pdo->prepare(
|
||||
'INSERT INTO tenant_memberships (tenant_id, user_id, role, status, joined_at) VALUES (?, ?, ?, ?, NOW())'
|
||||
)->execute([$tenantId, $userId, $role, 'active']);
|
||||
|
||||
$pdo->prepare(
|
||||
'INSERT INTO participants (tenant_id, user_id, display_name, email, email_norm, active) VALUES (?, ?, ?, ?, ?, 1)'
|
||||
)->execute([$tenantId, $userId, "Rolematrix {$role}", $email, $email]);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
fwrite(STDERR, "M8 role matrix setup failed: {$e->getMessage()}\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$failures = [];
|
||||
$passes = 0;
|
||||
|
||||
foreach ($roles as $role) {
|
||||
$cookies = [];
|
||||
|
||||
$loginPage = m8rm_request("{$baseUrl}/login.php", $cookies);
|
||||
preg_match('/name="csrf_token" value="([^"]+)"/', $loginPage['body'], $matches);
|
||||
$csrf = $matches[1] ?? '';
|
||||
|
||||
$postBody = http_build_query([
|
||||
'csrf_token' => $csrf,
|
||||
'email' => "rolematrix-{$role}-{$suffix}@test.local",
|
||||
'password' => $password,
|
||||
'tenant_slug' => "rolematrix-{$suffix}",
|
||||
]);
|
||||
m8rm_request("{$baseUrl}/login.php", $cookies, 'POST', $postBody);
|
||||
|
||||
foreach ($pages as $page => $allowedRoles) {
|
||||
$response = m8rm_request("{$baseUrl}/{$page}", $cookies);
|
||||
|
||||
$shouldHaveAccess = in_array($role, $allowedRoles, true);
|
||||
$looksGranted = !m8rm_looks_denied($response['body']);
|
||||
|
||||
$label = "{$role} auf {$page}";
|
||||
if ($shouldHaveAccess === $looksGranted) {
|
||||
$passes++;
|
||||
echo "PASS {$label}\n";
|
||||
} else {
|
||||
$failures[] = $shouldHaveAccess
|
||||
? "{$label}: Zugriff erwartet, aber verweigert"
|
||||
: "{$label}: Zugriff verweigert erwartet, aber gewährt";
|
||||
echo "FAIL {$label}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aufraeumen
|
||||
$pdo->prepare('DELETE FROM participants WHERE tenant_id = ?')->execute([$tenantId]);
|
||||
$pdo->prepare('DELETE FROM tenant_memberships WHERE tenant_id = ?')->execute([$tenantId]);
|
||||
foreach ($userIds as $uid) {
|
||||
$pdo->prepare('DELETE FROM user_auth_tokens WHERE user_id = ?')->execute([$uid]);
|
||||
$pdo->prepare('DELETE FROM users WHERE id = ?')->execute([$uid]);
|
||||
}
|
||||
$pdo->prepare('DELETE FROM tenant_settings WHERE tenant_id = ?')->execute([$tenantId]);
|
||||
$pdo->prepare('DELETE FROM tenants WHERE id = ?')->execute([$tenantId]);
|
||||
|
||||
if ($failures !== []) {
|
||||
echo "\nM8 role matrix check failed with " . count($failures) . " failure(s):\n";
|
||||
foreach ($failures as $failure) {
|
||||
echo "- {$failure}\n";
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "\nM8 role matrix check passed with {$passes} assertions.\n";
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/dev-db.php';
|
||||
require __DIR__ . '/../app/ledger.php';
|
||||
require __DIR__ . '/../app/saas-auth.php';
|
||||
require __DIR__ . '/../app/notices.php';
|
||||
require __DIR__ . '/../app/audit.php';
|
||||
|
||||
function m8_assert(string $label, bool $condition, array &$failures, int &$passes): void
|
||||
{
|
||||
if ($condition) {
|
||||
$passes++;
|
||||
echo "PASS {$label}\n";
|
||||
return;
|
||||
}
|
||||
|
||||
$failures[] = $label;
|
||||
echo "FAIL {$label}\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Some app functions (e.g. saas_grant_participant_access) manage their own
|
||||
* PDO transaction, so this script cannot wrap everything in one outer
|
||||
* transaction. Cleanup runs in a finally block instead.
|
||||
*/
|
||||
function m8_cleanup(PDO $pdo, array $tenantIds): void
|
||||
{
|
||||
foreach ($tenantIds as $tid) {
|
||||
$pdo->prepare('DELETE FROM audit_log WHERE tenant_id = ?')->execute([$tid]);
|
||||
$pdo->prepare('DELETE FROM notices WHERE tenant_id = ?')->execute([$tid]);
|
||||
$pdo->prepare('DELETE FROM ledger_entries WHERE tenant_id = ?')->execute([$tid]);
|
||||
$pdo->prepare('DELETE FROM participants WHERE tenant_id = ?')->execute([$tid]);
|
||||
$pdo->prepare('DELETE FROM tenant_settings WHERE tenant_id = ?')->execute([$tid]);
|
||||
$pdo->prepare('DELETE FROM tenants WHERE id = ?')->execute([$tid]);
|
||||
}
|
||||
}
|
||||
|
||||
$pdo = dev_pdo();
|
||||
$failures = [];
|
||||
$passes = 0;
|
||||
$suffix = bin2hex(random_bytes(4));
|
||||
$tenantIds = [];
|
||||
|
||||
try {
|
||||
// Zwei isolierte Test-Mandanten mit je einem Teilnehmer, einer Buchung,
|
||||
// einem Hinweis und einem Audit-Log-Eintrag anlegen.
|
||||
$participantIds = [];
|
||||
foreach (['a', 'b'] as $key) {
|
||||
$stmt = $pdo->prepare('INSERT INTO tenants (slug, name, status) VALUES (?, ?, ?)');
|
||||
$stmt->execute(["isolation-{$key}-{$suffix}", "Isolation Tenant {$key}", 'active']);
|
||||
$tenantIds[$key] = (int)$pdo->lastInsertId();
|
||||
|
||||
$pdo->prepare('INSERT INTO tenant_settings (tenant_id) VALUES (?)')->execute([$tenantIds[$key]]);
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO participants (tenant_id, display_name, email, email_norm, active) VALUES (?, ?, ?, ?, 1)'
|
||||
);
|
||||
$email = "isolation-{$key}-{$suffix}@test.local";
|
||||
$stmt->execute([$tenantIds[$key], "Isolation {$key}", $email, $email]);
|
||||
$participantIds[$key] = (int)$pdo->lastInsertId();
|
||||
|
||||
ledger_record_payment($pdo, $tenantIds[$key], $participantIds[$key], 1000, 'isolation_test');
|
||||
notices_create($pdo, $tenantIds[$key], "Hinweis {$key}", date('Y-m-d H:i:s', time() + 3600), null);
|
||||
app_audit_log($pdo, $tenantIds[$key], null, 'isolation_test.marker', 'test', null);
|
||||
}
|
||||
|
||||
$tenantA = $tenantIds['a'];
|
||||
$tenantB = $tenantIds['b'];
|
||||
$participantA = $participantIds['a'];
|
||||
|
||||
// 1. Teilnehmerlisten sind strikt getrennt.
|
||||
$summariesB = ledger_fetch_participant_summaries($pdo, $tenantB, ['active_only' => true]);
|
||||
$namesB = array_column($summariesB, 'display_name');
|
||||
m8_assert('tenant B sieht nur eigenen Teilnehmer', in_array('Isolation b', $namesB, true) && !in_array('Isolation a', $namesB, true), $failures, $passes);
|
||||
|
||||
// 2. Direkter Zugriff auf einen fremden Teilnehmer ueber den falschen Mandanten liefert nichts.
|
||||
$crossSummary = ledger_fetch_participant_summary($pdo, $tenantB, $participantA);
|
||||
m8_assert('fremder Teilnehmer ueber falschen Mandanten nicht lesbar', $crossSummary === null, $failures, $passes);
|
||||
|
||||
// 3. Letzte Buchungen sind mandantenscoped.
|
||||
$recentB = ledger_fetch_recent_entries($pdo, $tenantB, ['limit' => 50]);
|
||||
$participantIdsInRecentB = array_column($recentB, 'participant_id');
|
||||
m8_assert('letzte Buchungen zeigen keinen fremden Teilnehmer', !in_array($participantA, $participantIdsInRecentB, true), $failures, $passes);
|
||||
|
||||
// 4. Aktive Hinweise sind mandantenscoped.
|
||||
$noticeB = notices_fetch_active($pdo, $tenantB);
|
||||
m8_assert('aktiver Hinweis ist der eigene', $noticeB !== null && $noticeB['message'] === 'Hinweis b', $failures, $passes);
|
||||
|
||||
// 5. Schreibversuch auf einen fremden Teilnehmer ueber den falschen Mandanten schlaegt fehl.
|
||||
$crossWriteFailed = false;
|
||||
try {
|
||||
ledger_record_payment($pdo, $tenantB, $participantA, 500, 'isolation_test');
|
||||
} catch (Throwable $e) {
|
||||
$crossWriteFailed = true;
|
||||
}
|
||||
m8_assert('Buchung auf fremden Teilnehmer ueber falschen Mandanten schlaegt fehl', $crossWriteFailed, $failures, $passes);
|
||||
|
||||
// 6. Zugangsvergabe auf einen fremden Teilnehmer ueber den falschen Mandanten schlaegt fehl.
|
||||
$grantResult = saas_grant_participant_access($pdo, $tenantB, $participantA, 'member');
|
||||
m8_assert('Zugangsvergabe auf fremden Teilnehmer schlaegt fehl', $grantResult['ok'] === false, $failures, $passes);
|
||||
|
||||
// 7. Storno eines fremden Eintrags ueber den falschen Mandanten aendert nichts.
|
||||
// ledger_void_entry_by_legacy_id() matcht ueber (tenant_id, legacy_table,
|
||||
// legacy_id); dafuer braucht es einen Eintrag mit einer synthetischen
|
||||
// Legacy-Referenz, unabhaengig von echten kl_*-Zeilen.
|
||||
$pdo->prepare(
|
||||
"UPDATE ledger_entries SET legacy_table = 'isolation_test', legacy_id = ? WHERE tenant_id = ? AND participant_id = ?"
|
||||
)->execute([$participantA, $tenantA, $participantA]);
|
||||
$voidedWrongTenant = ledger_void_entry_by_legacy_id($pdo, $tenantB, 'isolation_test', $participantA);
|
||||
m8_assert('Storno ueber falschen Mandanten aendert nichts', $voidedWrongTenant === false, $failures, $passes);
|
||||
$voidedRightTenant = ledger_void_entry_by_legacy_id($pdo, $tenantA, 'isolation_test', $participantA);
|
||||
m8_assert('Storno ueber den richtigen Mandanten funktioniert', $voidedRightTenant === true, $failures, $passes);
|
||||
|
||||
// 8. Audit-Log ist mandantenscoped.
|
||||
$auditB = app_fetch_audit_log($pdo, $tenantB, 50);
|
||||
$auditActionsB = array_column($auditB, 'action');
|
||||
m8_assert('Audit-Log von Mandant B enthaelt nur eigene Eintraege', in_array('isolation_test.marker', $auditActionsB, true) && count($auditB) === 1, $failures, $passes);
|
||||
|
||||
// 9. Tenant-Settings sind getrennt (unterschiedliche IDs, kein Ueberlauf).
|
||||
$settingsA = saas_fetch_tenant_settings($pdo, $tenantA);
|
||||
$settingsB = saas_fetch_tenant_settings($pdo, $tenantB);
|
||||
m8_assert('tenant_settings sind pro Mandant getrennt', $settingsA['tenant_id'] !== $settingsB['tenant_id'], $failures, $passes);
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
fwrite(STDERR, "M8 tenant isolation check failed to run: {$e->getMessage()}\n");
|
||||
m8_cleanup($pdo, $tenantIds);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
m8_cleanup($pdo, $tenantIds);
|
||||
|
||||
if ($failures !== []) {
|
||||
echo "\nM8 tenant isolation check failed with " . count($failures) . " failure(s):\n";
|
||||
foreach ($failures as $failure) {
|
||||
echo "- {$failure}\n";
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "\nM8 tenant isolation check passed with {$passes} assertions.\n";
|
||||
Reference in New Issue
Block a user