OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

IntunePermissionCheckTest.php

7 KB
<?php

declare(strict_types=1);

use OpenNIT\Services\IntuneSyncService;

/**
 * Deckt die Prüfung der Entra-Berechtigungen ab.
 *
 * Der häufigste Grund für einen scheiternden Abgleich ist eine Berechtigung,
 * die zwar eingetragen, aber nie bestätigt wurde. Microsoft nimmt in den
 * Anspruch `roles` des App-Tokens genau die auf, die vergeben **und**
 * bestätigt sind – daran hängt die Prüfung.
 */
return static function (): void {

    /** Baut ein Token, wie Microsoft es ausstellt: Kopf, Inhalt, Signatur. */
    $token = static function (array $inhalt): string {
        $teil = static fn(array $d): string
            => rtrim(strtr(base64_encode(json_encode($d, JSON_UNESCAPED_SLASHES)), '+/', '-_'), '=');
        return $teil(['alg' => 'RS256', 'typ' => 'JWT']) . '.' . $teil($inhalt) . '.' . 'Unterschrift';
    };

    test('Die vergebenen Rollen werden aus dem Token gelesen', function () use ($token) {
        $rollen = IntuneSyncService::rollenAusToken($token([
            'aud'   => 'https://graph.microsoft.com',
            'roles' => ['DeviceManagementManagedDevices.Read.All', 'User.Read.All'],
        ]));
        assertSame(['DeviceManagementManagedDevices.Read.All', 'User.Read.All'], $rollen);
    });

    test('Ein Token ganz ohne Rollen ergibt eine leere Liste', function () use ($token) {
        // Genau so sieht es aus, wenn die Zustimmung fehlt.
        assertSame([], IntuneSyncService::rollenAusToken($token(['aud' => 'x'])));
    });

    test('Ein unbrauchbares Token ergibt nichts', function () {
        assertSame(null, IntuneSyncService::rollenAusToken('kein-token'));
        assertSame(null, IntuneSyncService::rollenAusToken('a.b'));
        assertSame(null, IntuneSyncService::rollenAusToken('a.!!!.c'));
        assertSame(null, IntuneSyncService::rollenAusToken(''));
    });

    /** Dienst ohne Datenbank; gesetzt werden nur die beiden Schalter. */
    $dienst = static function (bool $rueckschreiben, bool $person): IntuneSyncService {
        $svc = (new ReflectionClass(IntuneSyncService::class))->newInstanceWithoutConstructor();
        setPrivate($svc, 'writebackDeviceName', $rueckschreiben);
        setPrivate($svc, 'primaryUser', $person);
        return $svc;
    };

    test('Der Lesezugriff auf Geraete wird immer verlangt', function () use ($dienst) {
        foreach ([[false, false], [true, false], [false, true], [true, true]] as [$r, $p]) {
            $noetig = $dienst($r, $p)->benoetigteBerechtigungen();
            assertTrue(isset($noetig['DeviceManagementManagedDevices.Read.All']),
                'Die Grundberechtigung fehlt bei ' . var_export([$r, $p], true));
        }
    });

    test('Ohne Zusatzfunktionen genuegt das Leserecht', function () use ($dienst) {
        assertSame(['DeviceManagementManagedDevices.Read.All'],
            array_keys($dienst(false, false)->benoetigteBerechtigungen()));
    });

    test('Rueckschreiben verlangt das Schreibrecht', function () use ($dienst) {
        $noetig = $dienst(true, false)->benoetigteBerechtigungen();
        assertTrue(isset($noetig['DeviceManagementManagedDevices.ReadWrite.All']),
            'Ohne Schreibrecht: ' . implode(', ', array_keys($noetig)));
    });

    test('Die Personenaufloesung verlangt das Leserecht auf Personen', function () use ($dienst) {
        $noetig = $dienst(false, true)->benoetigteBerechtigungen();
        assertTrue(isset($noetig['User.Read.All']),
            'Ohne Personenrecht: ' . implode(', ', array_keys($noetig)));
        // Wer die Person nicht aufloest, braucht das Recht auch nicht.
        assertFalse(isset($dienst(false, false)->benoetigteBerechtigungen()['User.Read.All']),
            'Das Personenrecht wird ohne Not verlangt');
    });

    test('Die Einstellungen bestimmen, was zusaetzlich noetig ist', function () {
        $wurzel  = dirname(__DIR__);
        $quelle  = (string)file_get_contents($wurzel . '/src/Services/IntuneSyncService.php');

        // Rückschreiben braucht Schreibrechte, die Personenauflösung User.Read.All.
        assertTrue(str_contains($quelle, 'DeviceManagementManagedDevices.ReadWrite.All'),
            'Das Schreibrecht wird nirgends verlangt');
        assertTrue(str_contains($quelle, "'User.Read.All'"),
            'Das Leserecht auf Personen wird nirgends verlangt');
        // Beide hängen an ihrer jeweiligen Einstellung, nicht am Grundfall.
        assertTrue(str_contains($quelle, 'if ($this->writebackDeviceName)'), 'Schreibrecht ohne Bedingung');
        assertTrue(str_contains($quelle, 'if ($this->primaryUser)'), 'Personenrecht ohne Bedingung');
    });

    test('Ein Schreibrecht deckt das zugehoerige Leserecht ab', function () {
        // Wer ReadWrite hat, braucht Read nicht zusätzlich eingetragen.
        $quelle = (string)file_get_contents(dirname(__DIR__) . '/src/Services/IntuneSyncService.php');
        assertTrue(str_contains($quelle, "str_replace('.Read.All', '.ReadWrite.All', \$name)"),
            'Die Abdeckung durch das Schreibrecht fehlt');
    });

    test('Der Knopf steht auf der Seite und haengt an einer Route', function () {
        $wurzel = dirname(__DIR__);
        $tpl    = (string)file_get_contents($wurzel . '/templates/admin/inventory/intune.php');
        $routen = (string)file_get_contents($wurzel . '/src/Core/Application.php');

        assertTrue(str_contains($tpl, 'id="int-perm-btn"'), 'Der Knopf fehlt');
        assertTrue(str_contains($tpl, '/admin/inventory/intune/permissions'), 'Der Knopf ruft nichts auf');
        assertTrue(str_contains($routen, "'/admin/inventory/intune/permissions'"), 'Die Route fehlt');
        // Wie der übrige Bereich: nur mit dem Recht für die Inventarverwaltung.
        $zeile = substr($routen, (int)strpos($routen, "'/admin/inventory/intune/permissions'"), 220);
        assertTrue(str_contains($zeile, "'capability' => 'admin_assets'"), 'Die Route verlangt kein Recht');
    });

    test('Fremde Angaben gehen maskiert in die Seite', function () {
        // Die Namen kommen von Microsoft; sie landen im Seitentext.
        $tpl = (string)file_get_contents(dirname(__DIR__) . '/templates/admin/inventory/intune.php');
        assertTrue(str_contains($tpl, 'function esc(t)'), 'Der Maskier-Helfer fehlt');
        assertTrue(str_contains($tpl, "esc(f.name)"), 'Der Name wird ungefiltert eingesetzt');
        assertTrue(str_contains($tpl, "esc(g)"), 'Die Liste wird ungefiltert eingesetzt');
    });

    test('Die Pruefung steht im Audit-Log', function () {
        $quelle = (string)file_get_contents(dirname(__DIR__) . '/src/Controllers/Admin/IntuneAdminController.php');
        assertTrue(str_contains($quelle, 'intune_permissions_checked'), 'Ohne Eintrag im Audit-Log');
    });
};