275 lines
9.1 KiB
PHP
275 lines
9.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App;
|
|
|
|
final class RegistrationService
|
|
{
|
|
private RegistrationStore $store;
|
|
private RegistrationLdifBuilder $ldifBuilder;
|
|
|
|
/**
|
|
* @param array<string, mixed> $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<int, array<string, mixed>>
|
|
*/
|
|
public function all(): array
|
|
{
|
|
return $this->store->all();
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
public function find(string $id): ?array
|
|
{
|
|
return $this->store->find($id);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $input
|
|
* @return array{success: bool, errors?: array<int, string>, request?: array<string, mixed>}
|
|
*/
|
|
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<string, string> $input
|
|
* @return array{success: bool, errors?: array<int, string>, request?: array<string, mixed>}
|
|
*/
|
|
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<int, string>, request?: array<string, mixed>}
|
|
*/
|
|
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, '/');
|
|
}
|
|
}
|