Quelltext des aktuellen Standes
<?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');
});
};