$config */ public function __construct( private readonly string $projectRoot, private readonly array $config, ) { $storagePath = $this->resolveProjectPath((string) ($this->config['storage_path'] ?? 'data/registration-requests.json')); $this->store = new RegistrationStore($storagePath); $this->ldifBuilder = new RegistrationLdifBuilder($this->config); } public function isEnabled(): bool { return (bool) ($this->config['enabled'] ?? false); } /** * @return array> */ public function all(): array { return $this->store->all(); } /** * @return array|null */ public function find(string $id): ?array { return $this->store->find($id); } /** * @param array $input * @return array{success: bool, errors?: array, request?: array} */ public function submit(array $input): array { $username = strtolower(trim((string) ($input['username'] ?? ''))); $givenName = trim((string) ($input['given_name'] ?? '')); $familyName = trim((string) ($input['family_name'] ?? '')); $email = strtolower(trim((string) ($input['email'] ?? ''))); $note = trim((string) ($input['note'] ?? '')); $displayName = trim($givenName . ' ' . $familyName); $errors = []; if (!preg_match('/^[a-z0-9._-]{3,32}$/', $username)) { $errors[] = 'Der Benutzername muss 3 bis 32 Zeichen lang sein und darf nur Kleinbuchstaben, Zahlen, Punkt, Minus oder Unterstrich enthalten.'; } if ($givenName === '') { $errors[] = 'Der Vorname ist erforderlich.'; } if ($familyName === '') { $errors[] = 'Der Nachname ist erforderlich.'; } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { $errors[] = 'Bitte gib eine gültige E-Mail-Adresse an.'; } if ($note !== '' && mb_strlen($note) > 2000) { $errors[] = 'Die Zusatzinfo ist zu lang.'; } $conflict = $this->store->findConflict($username, $email); if ($conflict !== null) { $errors[] = 'Für diesen Benutzernamen oder diese E-Mail existiert bereits eine offene oder freigegebene Registrierung.'; } if ($errors !== []) { return [ 'success' => false, 'errors' => $errors, ]; } $request = [ 'id' => bin2hex(random_bytes(8)), 'status' => 'pending', 'created_at' => gmdate('c'), 'updated_at' => gmdate('c'), 'username' => $username, 'given_name' => $givenName, 'family_name' => $familyName, 'display_name' => $displayName, 'email' => $email, 'note' => $note, 'origin' => [ 'ip' => (string) ($_SERVER['REMOTE_ADDR'] ?? ''), 'user_agent' => (string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 'host' => (string) ($_SERVER['HTTP_HOST'] ?? ''), ], 'review' => null, 'provisioning' => null, ]; return [ 'success' => true, 'request' => $this->store->create($request), ]; } /** * @param array $input * @return array{success: bool, errors?: array, request?: array} */ public function approve(string $id, array $input, string $reviewer): array { $request = $this->store->find($id); if ($request === null) { return ['success' => false, 'errors' => ['Die Registrierung wurde nicht gefunden.']]; } $uidNumber = trim((string) ($input['uid_number'] ?? '')); $gidNumber = trim((string) ($input['gid_number'] ?? '')); $homeDirectory = trim((string) ($input['home_directory'] ?? '')); $sambaSid = trim((string) ($input['samba_sid'] ?? '')); $memberOfCsv = trim((string) ($input['member_of_csv'] ?? '')); $adminNote = trim((string) ($input['admin_note'] ?? '')); $errors = []; if ($uidNumber === '' || !ctype_digit($uidNumber)) { $errors[] = 'uidNumber ist erforderlich und muss numerisch sein.'; } if ($gidNumber === '' || !ctype_digit($gidNumber)) { $errors[] = 'gidNumber ist erforderlich und muss numerisch sein.'; } if ($homeDirectory === '') { $errors[] = 'homeDirectory ist erforderlich.'; } if ($errors !== []) { return ['success' => false, 'errors' => $errors]; } $provisioning = [ 'uid_number' => $uidNumber, 'gid_number' => $gidNumber, 'home_directory' => $homeDirectory, 'login_shell' => trim((string) ($input['login_shell'] ?? ($this->config['ldap']['default_login_shell'] ?? '/bin/sh'))), 'member_of_csv' => $memberOfCsv, 'samba_sid' => $sambaSid, 'apple_generateduid' => trim((string) ($input['apple_generateduid'] ?? '')), 'display_name' => trim((string) ($input['display_name'] ?? (string) ($request['display_name'] ?? ''))), 'cn' => trim((string) ($input['cn'] ?? (string) ($request['display_name'] ?? $request['username'] ?? ''))), 'gecos' => trim((string) ($input['gecos'] ?? (string) ($request['display_name'] ?? ''))), 'admin_note' => $adminNote, 'ldif_generated_at' => gmdate('c'), ]; $request = $this->store->update( $id, static function (array $item) use ($provisioning, $reviewer, $adminNote): array { $item['status'] = 'approved_pending_provisioning'; $item['updated_at'] = gmdate('c'); $item['review'] = [ 'reviewed_at' => gmdate('c'), 'reviewed_by' => $reviewer, 'decision' => 'approved', 'note' => $adminNote, ]; $item['provisioning'] = $provisioning; return $item; } ); if ($request === null) { return ['success' => false, 'errors' => ['Die Registrierung konnte nicht aktualisiert werden.']]; } $ldif = $this->ldifBuilder->build($request, $provisioning); $ldifPath = $this->writeLdifDraft((string) ($request['id'] ?? ''), (string) ($request['username'] ?? ''), $ldif); $request = $this->store->update( $id, static function (array $item) use ($ldifPath): array { $item['updated_at'] = gmdate('c'); $item['provisioning']['ldif_path'] = $ldifPath; $item['provisioning']['status'] = 'ldif_generated'; return $item; } ); return [ 'success' => true, 'request' => $request, ]; } /** * @return array{success: bool, errors?: array, request?: array} */ public function reject(string $id, string $reason, string $reviewer): array { $reason = trim($reason); if ($reason === '') { return ['success' => false, 'errors' => ['Bitte gib einen Ablehnungsgrund an.']]; } $request = $this->store->update( $id, static function (array $item) use ($reason, $reviewer): array { $item['status'] = 'rejected'; $item['updated_at'] = gmdate('c'); $item['review'] = [ 'reviewed_at' => gmdate('c'), 'reviewed_by' => $reviewer, 'decision' => 'rejected', 'note' => $reason, ]; return $item; } ); if ($request === null) { return ['success' => false, 'errors' => ['Die Registrierung wurde nicht gefunden.']]; } return [ 'success' => true, 'request' => $request, ]; } private function writeLdifDraft(string $id, string $username, string $content): string { $directory = $this->resolveProjectPath((string) ($this->config['ldif_export_dir'] ?? 'data/registration-ldif')); if (!is_dir($directory)) { mkdir($directory, 0775, true); } $filename = $id . '-' . preg_replace('/[^a-z0-9._-]+/i', '-', $username) . '.ldif'; $path = rtrim($directory, '/') . '/' . $filename; file_put_contents($path, $content, LOCK_EX); return $path; } private function resolveProjectPath(string $path): string { if ($path === '') { return $this->projectRoot . '/data/registration-requests.json'; } if (str_starts_with($path, '/')) { return $path; } return $this->projectRoot . '/' . ltrim($path, '/'); } }