Die Zugriffspruefung lief bisher in 19 Dateien auf einen Fallback gegen die Legacy-Tabelle kl_Mitarbeiter (checkKaffeelisteAdmin/-Access). Der Zugang haengt jetzt ausschliesslich an der SaaS-Anmeldung. - Fallback-Bloecke in allen 19 Dateien entfernt, ebenso getUserName/getUserId - functions.php besteht nur noch aus der SaaS-Anmeldung; die per String zusammengebaute Abfrage "WHERE Email like '<eingabe>'" ist damit weg - config.php baut keine sqlsrv-Verbindung mehr auf, der Kompatibilitaetslayer lib/sqlsrv_mysql_compat.php ist verwaist und entfaellt - Auto-Login per DEV_AUTH_EMAIL gibt es nicht mehr: er meldete jeden Besucher an und machte damit den Login-Schutz unpruefbar. Angemeldet wird auch lokal ueber login.php; die Variable dient nur noch scripts/init-mysql-dev.php http-smoke war an die Golden-Master-Daten des Default-Mandanten gebunden und hatte deshalb sechs dauerhaft rote Pruefungen. Der Test legt sich jetzt einen eigenen Mandanten mit bekannten Salden an, meldet sich per HTTP an und raeumt danach auf. Geschuetzte Seiten werden nicht mehr ueber das Wort "Login" im Text geprueft, sondern ueber die tatsaechliche 302-Umleitung. http-smoke 27 PASS / 6 FAIL -> 33 PASS / 0 FAIL role-matrix 55 PASS / 0 FAIL -> unveraendert tenant-isolation 9 PASS / 1 FAIL -> 11 PASS / 1 FAIL (Altfehler) Ausserdem "keine Zugang" -> "keinen Zugang" in zwei Fehlermeldungen; der Rollentest erkannte die Ablehnung an genau diesem Tippfehler und wurde mitgezogen. Co-Authored-By: Claude Opus 4.8 <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', 'keinen 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";
|