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>
184 lines
6.4 KiB
PHP
184 lines
6.4 KiB
PHP
<?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";
|