Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
/**
* Hält fest, welche Berechtigung die Administrationsbereiche verlangen.
*
* Systemnahe Bereiche haben eigene Rechte, damit für Cronjobs, Protokolle oder
* den Mailversand niemand die globale Benutzerverwaltung braucht – über die er
* sich sonst selbst zum Administrator machen könnte.
*/
return static function (): void {
/** Alle registrierten Routen als [methode, pfad, ziel, capability]. */
$routen = static function (): array {
$src = (string)file_get_contents(dirname(__DIR__) . '/src/Core/Application.php');
preg_match_all(
'/router->(get|post|put|delete|patch)\(\s*\'([^\']+)\'\s*,\s*\'([^\']+)\'\s*(?:,\s*\[(.*?)\])?\s*\)/s',
$src,
$treffer,
PREG_SET_ORDER
);
$out = [];
foreach ($treffer as $t) {
$cap = preg_match('/\'capability\'\s*=>\s*\'([^\']+)\'/', $t[4] ?? '', $c) ? $c[1] : null;
$out[] = [strtoupper($t[1]), $t[2], $t[3], $cap];
}
return $out;
};
/** Die neuen Rechte stehen im Katalog und werden an den Bestand vergeben. */
test('Die systemnahen Berechtigungen sind angelegt', function () {
$sql = (string)file_get_contents(
dirname(__DIR__) . '/database/migrations/242_admin_capabilities.sql'
);
foreach (['admin_system', 'admin_settings', 'admin_api', 'admin_email', 'admin_docs', 'view_manual'] as $cap) {
assertTrue(str_contains($sql, "'" . $cap . "'"), 'Berechtigung fehlt: ' . $cap);
}
// Ohne Übernahme verlöre jede bestehende Rolle ihre Zugänge.
assertTrue(str_contains($sql, "alt.name = 'manage_users'"), 'Der Bestand wird nicht übernommen');
assertTrue(str_contains($sql, 'INSERT IGNORE INTO `role_capabilities`'), 'Die Vergabe fehlt');
});
test('manage_users sichert nur noch Benutzer und Verzeichnis', function () use ($routen) {
// Das Verzeichnis legt Personen an und macht daraus Benutzerkonten –
// es gehört zur Benutzerverwaltung.
$fremd = [];
foreach ($routen() as [$methode, $pfad, $ziel, $cap]) {
if ($cap !== 'manage_users') { continue; }
if (str_starts_with($pfad, '/admin/users') || str_starts_with($pfad, '/admin/directory')) { continue; }
$fremd[] = $methode . ' ' . $pfad;
}
assertSame([], $fremd);
});
test('Jeder umgehängte Bereich trägt seine eigene Berechtigung', function () use ($routen) {
$erwartet = [
'/admin/system-health' => 'admin_system',
'/admin/cronjobs' => 'admin_system',
'/admin/logs' => 'admin_system',
'/admin/image-optimization' => 'admin_system',
'/admin/api' => 'admin_api',
'/admin/email' => 'admin_email',
'/admin/notifications' => 'admin_email',
'/admin/tags' => 'admin_docs',
'/admin/page-templates' => 'admin_docs',
'/admin/archived-pages' => 'admin_docs',
'/admin/tinymce' => 'admin_docs',
'/admin/settings/security' => 'admin_settings',
'/admin/manual' => 'view_manual',
'/admin/compliance' => 'view_audit_log',
];
$abweichungen = [];
foreach ($routen() as [$methode, $pfad, $ziel, $cap]) {
foreach ($erwartet as $praefix => $soll) {
if ($pfad !== $praefix && !str_starts_with($pfad, $praefix . '/')) { continue; }
if ($cap !== $soll) {
$abweichungen[] = $methode . ' ' . $pfad . ': ' . ($cap ?? 'keine') . ' statt ' . $soll;
}
}
}
assertSame([], $abweichungen);
});
test('Die Prüfung im Controller passt zur Route', function () use ($routen) {
// Eine Route, die weiter kommt als der Controller, endet für den
// Berechtigten in einem 403 statt in der Seite.
$abweichungen = [];
foreach ($routen() as [$methode, $pfad, $ziel, $cap]) {
if ($cap === null || !str_starts_with($pfad, '/admin/')) { continue; }
[$klasse, $aktion] = array_pad(explode('@', $ziel, 2), 2, '');
$datei = dirname(__DIR__) . '/src/Controllers/' . str_replace('\\', '/', $klasse) . '.php';
if (!is_file($datei)) { continue; }
$src = (string)file_get_contents($datei);
$start = strpos($src, 'function ' . $aktion . '(');
if ($start === false) { continue; }
$rumpf = substr($src, $start, 900);
if (preg_match_all('/authorize\(\s*\'([a-z_]+)\'\s*\)/', $rumpf, $m) === 0) { continue; }
// Mehrere Prüfungen im Rumpf: eine muss zur Route passen.
if (!in_array($cap, $m[1], true)) {
$abweichungen[] = $pfad . ': Route ' . $cap . ', Controller ' . implode('/', $m[1]);
}
}
assertSame([], $abweichungen);
});
test('Kein Administrationsbereich steht ohne Prüfung da', function () use ($routen) {
// Ohne Capability an der Route muss der Controller selbst prüfen.
$ungeschuetzt = [];
foreach ($routen() as [$methode, $pfad, $ziel, $cap]) {
if ($cap !== null || !str_starts_with($pfad, '/admin')) { continue; }
[$klasse] = array_pad(explode('@', $ziel, 2), 2, '');
$datei = dirname(__DIR__) . '/src/Controllers/' . str_replace('\\', '/', $klasse) . '.php';
if (!is_file($datei)) { $ungeschuetzt[] = $methode . ' ' . $pfad; continue; }
$src = (string)file_get_contents($datei);
$prueft = str_contains($src, 'authorize(')
|| str_contains($src, 'requireCapability(')
|| str_contains($src, 'canAccessAdminArea(');
if (!$prueft) { $ungeschuetzt[] = $methode . ' ' . $pfad; }
}
assertSame([], $ungeschuetzt);
});
};