Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
use OpenNIT\Core\Database;
use OpenNIT\Workflows\WorkflowManager;
/**
* Deckt ab, für wen ein Antrag gestellt werden darf.
*
* Ohne Einschränkung kann jede antragsberechtigte Person jede beliebige Person
* aus dem Verzeichnis als betroffen eintragen – auch für einen Austritt. Die
* Vorlage lässt sich deshalb auf die eigenen Unterstellten begrenzen.
*
* Entscheidend ist, dass die Prüfung beim Einreichen stattfindet: Eine
* gefilterte Auswahlliste ist nur Bequemlichkeit, denn die Kennung der
* betroffenen Person kommt aus dem abgesendeten Formular.
*/
return static function (): void {
/**
* Datenbank-Attrappe mit einem kleinen Verzeichnis.
*
* @param array<string,string> $benutzer Konto-Kennung => E-Mail
* @param list<array<string,mixed>> $personen Verzeichnis-Personen
*/
$db = static function (array $benutzer, array $personen): Database {
return new class ($benutzer, $personen) extends Database {
public function __construct(private array $benutzer, private array $personen) {}
public function fetchOne(string $sql, array $params = []): ?array
{
if (str_contains($sql, 'FROM users')) {
$email = $this->benutzer[(string)$params[0]] ?? null;
return $email === null ? null : ['email' => $email];
}
// Person über E-Mail
foreach ($this->personen as $p) {
if (($p['email'] ?? null) === ($params[0] ?? null) && (int)($p['is_active'] ?? 1) === 1) {
return $p;
}
}
return null;
}
public function fetchAll(string $sql, array $params = []): array
{
// Unterstellte: manager_email = ? ODER manager_ldap_dn = ?
[$email, $dn] = [$params[0] ?? '', $params[1] ?? ''];
$treffer = [];
foreach ($this->personen as $p) {
if ((int)($p['is_active'] ?? 1) !== 1) {
continue;
}
$perMail = ($p['manager_email'] ?? '') !== '' && $p['manager_email'] === $email;
$perDn = ($p['manager_ldap_dn'] ?? '') !== '' && $dn !== '' && $p['manager_ldap_dn'] === $dn;
if ($perMail || $perDn) {
$treffer[] = ['id' => $p['id']];
}
}
return $treffer;
}
};
};
// Chefin, zwei Unterstellte, eine fremde Person – einmal über Entra
// (manager_email), einmal über LDAP (manager_ldap_dn).
$verzeichnis = [
['id' => 1, 'email' => 'chefin@example', 'external_id' => 'CN=Chefin,OU=X', 'is_active' => 1],
['id' => 2, 'email' => 'anna@example', 'manager_email' => 'chefin@example', 'is_active' => 1],
['id' => 3, 'email' => 'bernd@example', 'manager_ldap_dn' => 'CN=Chefin,OU=X', 'is_active' => 1],
['id' => 4, 'email' => 'fremd@example', 'manager_email' => 'andere@example', 'is_active' => 1],
['id' => 5, 'email' => 'ex@example', 'manager_email' => 'chefin@example', 'is_active' => 0],
];
$konten = ['10' => 'chefin@example', '11' => 'fremd@example', '12' => 'ohne@verzeichnis'];
// WorkflowManager erwartet weitere Abhaengigkeiten – die Aufloesung braucht
// nur die Datenbank, daher ohne Konstruktor erzeugen und sie nachreichen.
$auflösung = static function (int $userId) use ($db, $konten, $verzeichnis): array {
$m = (new ReflectionClass(WorkflowManager::class))->newInstanceWithoutConstructor();
$prop = new ReflectionProperty(WorkflowManager::class, 'db');
$prop->setAccessible(true);
$prop->setValue($m, $db($konten, $verzeichnis));
return $m->selectablePersonIds($userId);
};
// ----------------------------------------------------------------
// Auflösung der Unterstellten
// ----------------------------------------------------------------
test('Die eigene Person ist immer wählbar', function () use ($auflösung) {
// Ein Antrag für sich selbst darf nie an der Einschränkung scheitern.
assertTrue(in_array(1, $auflösung(10), true));
});
test('Beide Verzeichnis-Wege liefern die Unterstellten', function () use ($auflösung) {
// Entra pflegt die E-Mail der Führungskraft, LDAP deren DN.
$ids = $auflösung(10);
assertTrue(in_array(2, $ids, true), 'Unterstellte über die E-Mail fehlt');
assertTrue(in_array(3, $ids, true), 'Unterstellter über den DN fehlt');
});
test('Fremde Personen sind nicht wählbar', function () use ($auflösung) {
assertFalse(in_array(4, $auflösung(10), true), 'Eine fremde Person ist wählbar');
});
test('Ausgetretene Personen sind nicht wählbar', function () use ($auflösung) {
assertFalse(in_array(5, $auflösung(10), true), 'Eine inaktive Person ist wählbar');
});
test('Wer niemanden führt, darf nur für sich selbst stellen', function () use ($auflösung) {
assertSame([4], $auflösung(11));
});
test('Wer nicht im Verzeichnis steht, bekommt keine Auswahl', function () use ($auflösung) {
// Ein reines Verwaltungskonto ohne Verzeichnis-Eintrag; die Ausnahme für
// die Vorlagen-Verwaltung greift eine Ebene höher im Controller.
assertSame([], $auflösung(12));
});
// ----------------------------------------------------------------
// Wo die Prüfung stattfindet
// ----------------------------------------------------------------
test('Beide Antragswege prüfen beim Einreichen, nicht nur die Liste', function () {
// Die Kennung der betroffenen Person kommt aus dem Formular; eine nur
// gefilterte Auswahlliste liesse sich mit einem eigenen Absenden umgehen.
$wurzel = dirname(__DIR__) . '/src/Controllers/';
foreach (['WorkflowController.php', 'PortalController.php'] as $datei) {
$quelle = (string)file_get_contents($wurzel . $datei);
$stelle = strpos($quelle, "_subject_person_id");
assertTrue($stelle !== false, "Kein Personenbezug in {$datei}");
$block = substr($quelle, $stelle, 900);
assertTrue(str_contains($block, 'erlaubteBetroffene('), "Keine Prüfung beim Einreichen in {$datei}");
assertTrue(str_contains($block, 'in_array($pid, $erlaubt, true)'), "Die Kennung wird in {$datei} nicht geprüft");
}
});
test('Auch die Personensuche folgt der Einschränkung', function () {
// Das Formular im Backend füllt die betroffene Person nicht aus einer
// Liste, sondern über die Personensuche. Ohne Einschränkung dort liesse
// sich jemand auswählen, den das Einreichen anschliessend abweist.
$suche = (string)file_get_contents(dirname(__DIR__) . '/src/Controllers/Admin/DirectoryAdminController.php');
assertTrue(str_contains($suche, "\$request->get('workflow', 0)"), 'Die Suche kennt den Vorlagen-Bezug nicht');
assertTrue(str_contains($suche, 'erlaubteBetroffeneFuerVorlage('), 'Die Suche wendet die Einschränkung nicht an');
$form = (string)file_get_contents(dirname(__DIR__) . '/templates/workflows/start.php');
assertTrue(
str_contains($form, '/api/persons/search?workflow='),
'Das Formular gibt der Suche die Vorlage nicht mit'
);
});
test('Ohne Einschränkung bleibt alles wie bisher', function () {
// Bestehende Vorlagen sollen sich durch das Update nicht ändern.
$sql = (string)file_get_contents(dirname(__DIR__) . '/database/migrations/218_workflow_subject_scope.sql');
assertTrue(str_contains($sql, "NOT NULL DEFAULT 'all'"), 'Der Standard ist nicht der bisherige Stand');
foreach (['WorkflowController.php', 'PortalController.php'] as $datei) {
$quelle = (string)file_get_contents(dirname(__DIR__) . '/src/Controllers/' . $datei);
assertTrue(
str_contains($quelle, "(\$wf['subject_scope'] ?? 'all') !== 'reports'"),
"In {$datei} fehlt der Ausweichwert für Instanzen ohne die Spalte"
);
}
});
test('Die Einschränkung lässt sich im Backend setzen', function () {
$seite = (string)file_get_contents(dirname(__DIR__) . '/templates/admin/workflows/edit.php');
assertTrue(str_contains($seite, 'name="subject_scope"'), 'Kein Schalter in der Vorlagen-Verwaltung');
$ctrl = (string)file_get_contents(dirname(__DIR__) . '/src/Controllers/Admin/WorkflowAdminController.php');
assertTrue(str_contains($ctrl, "'subject_scope' => \$subjectScope"), 'Der Schalter wird nicht gespeichert');
// Ohne Personenbezug ergibt die Einschränkung keinen Sinn und würde nur verwirren.
assertTrue(str_contains($ctrl, "\$subjectMode === 'person' && \$request->post('subject_scope') === 'reports'"), 'Die Einschränkung ist nicht an den Personenbezug gebunden');
});
};