OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

AdminCapabilityTest.php

6 KB
<?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);
    });
};