Files
kaffeekasse-saas/scripts/check-m8-role-matrix.php
T
clemensandClaude Opus 4.8 3e24910831 Legacy-Abbau Schritt 2: Legacy-Authentifizierung entfernt
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>
2026-07-21 21:10:09 +02:00

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";