asdasd
All checks were successful
Deploy / deploy-staging (push) Successful in 1m3s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-06-15 02:05:36 +02:00
parent e9b86eb7a3
commit 9325b93404
18 changed files with 1102 additions and 268 deletions

View File

@@ -29,13 +29,15 @@ final class App
$widgetRegistry = new WidgetRegistry($this->projectRoot . '/config/widgets.php');
$skins = SkinResolver::all();
$activeSkin = SkinResolver::resolve($_GET['skin'] ?? null);
$apps = AppIconResolver::decorate($registry->all(), $activeSkin);
$windows = (new WindowManager($registry))->defaultWindows();
$activeWidgetIds = DesktopState::defaultWidgetIds();
$isAuthenticated = isset($_SESSION['desktop_auth']) && is_array($_SESSION['desktop_auth']);
$authUser = $isAuthenticated && is_array($_SESSION['desktop_auth']['user'] ?? null)
? $_SESSION['desktop_auth']['user']
: [];
$authGroups = array_values(array_map('strval', (array) ($authUser['groups'] ?? [])));
$visibleApps = $this->filterAppsForGroups($registry->all(), $authGroups);
$apps = AppIconResolver::decorate($visibleApps, $activeSkin);
$windows = (new WindowManager($registry))->defaultWindows();
$activeWidgetIds = DesktopState::defaultWidgetIds();
$displayName = (string) ($authUser['name'] ?? $authUser['username'] ?? 'Gast');
return [
@@ -80,4 +82,30 @@ final class App
],
];
}
/**
* @param array<int, array<string, mixed>> $apps
* @param array<int, string> $groups
* @return array<int, array<string, mixed>>
*/
private function filterAppsForGroups(array $apps, array $groups): array
{
$normalizedGroups = array_map(static fn (string $group): string => strtolower(trim($group, '/')), $groups);
return array_values(array_filter($apps, static function (array $app) use ($normalizedGroups): bool {
$requiredGroups = array_values(array_map('strval', (array) ($app['required_groups'] ?? [])));
if ($requiredGroups === []) {
return true;
}
foreach ($requiredGroups as $group) {
if (in_array(strtolower(trim($group, '/')), $normalizedGroups, true)) {
return true;
}
}
return false;
}));
}
}

51
src/App/JsonStore.php Normal file
View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App;
final class JsonStore
{
public function __construct(
private readonly string $storagePath,
) {
}
/**
* @return array<int|string, mixed>
*/
public function read(): array
{
if (!is_file($this->storagePath)) {
return [];
}
$raw = file_get_contents($this->storagePath);
if ($raw === false || trim($raw) === '') {
return [];
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
/**
* @param array<int|string, mixed> $payload
*/
public function write(array $payload): void
{
$directory = dirname($this->storagePath);
if (!is_dir($directory)) {
mkdir($directory, 0775, true);
}
file_put_contents(
$this->storagePath,
json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL,
LOCK_EX
);
}
}

View File

@@ -56,6 +56,60 @@ final class LdapProvisioner
return 'uid=' . $username . ',' . $baseDn;
}
public function usernameExists(string $username): bool
{
return $this->getUserByUsername($username, ['uid']) !== null;
}
/**
* @param array<int, string> $attributes
* @return array<string, mixed>|null
*/
public function getUserByUsername(string $username, array $attributes = []): ?array
{
try {
$link = $this->connectAsService();
$userDn = $this->userDnForUsername($username);
$read = @ldap_read($link, $userDn, '(objectClass=*)', $attributes === [] ? ['*'] : $attributes);
if ($read === false) {
return null;
}
$entries = ldap_get_entries($link, $read);
if (!is_array($entries) || (int) ($entries['count'] ?? 0) < 1) {
return null;
}
$entry = ['dn' => $userDn];
foreach ($entries[0] as $key => $value) {
if (is_int($key) || $key === 'count') {
continue;
}
if (is_array($value)) {
if ((int) ($value['count'] ?? 0) === 1) {
$entry[$key] = (string) ($value[0] ?? '');
} else {
$items = [];
foreach ($value as $subKey => $subValue) {
if (is_int($subKey)) {
$items[] = (string) $subValue;
}
}
$entry[$key] = $items;
}
}
}
return $entry;
} catch (\Throwable) {
return null;
}
}
/**
* @return array{success: bool, message: string}
*/
@@ -172,7 +226,12 @@ final class LdapProvisioner
throw new RuntimeException('LDAP-Benutzer konnte nicht angelegt werden: ' . $this->lastError($link));
}
$groupUpdates = $this->assignGroups($link, (string) ($request['username'] ?? ''), (string) $userDn, $provisioning);
$groupUpdates = $this->syncGroupsForUser(
$link,
(string) ($request['username'] ?? ''),
(string) $userDn,
$this->resolveTargetGroupsFromProvisioning($provisioning)
);
return [
'dn' => $userDn,
@@ -180,6 +239,64 @@ final class LdapProvisioner
];
}
/**
* @param array<int, string> $targetGroups
* @return array{success: bool, message: string, dn?: string}
*/
public function activateUser(string $username, array $targetGroups): array
{
try {
$link = $this->connectAsService();
$userDn = $this->userDnForUsername($username);
$groupUpdates = $this->syncGroupsForUser($link, $username, $userDn, $targetGroups);
if (!@ldap_modify($link, $userDn, [
'shadowExpire' => (string) ($this->config['ldap']['active_shadow_expire'] ?? -1),
])) {
throw new RuntimeException('Aktivierung im LDAP fehlgeschlagen: ' . $this->lastError($link));
}
return [
'success' => true,
'message' => 'Benutzer wurde aktiviert.',
'dn' => $userDn,
];
} catch (\Throwable $exception) {
return [
'success' => false,
'message' => $exception->getMessage(),
];
}
}
/**
* @return array{success: bool, message: string, dn?: string}
*/
public function deactivateUser(string $username): array
{
try {
$link = $this->connectAsService();
$userDn = $this->userDnForUsername($username);
if (!@ldap_modify($link, $userDn, [
'shadowExpire' => (string) ($this->config['ldap']['shadow_expire'] ?? 1),
])) {
throw new RuntimeException('Deaktivierung im LDAP fehlgeschlagen: ' . $this->lastError($link));
}
return [
'success' => true,
'message' => 'Benutzer wurde deaktiviert.',
'dn' => $userDn,
];
} catch (\Throwable $exception) {
return [
'success' => false,
'message' => $exception->getMessage(),
];
}
}
/**
* @param array<string, mixed> $request
* @param array<string, mixed> $provisioning
@@ -255,14 +372,20 @@ final class LdapProvisioner
*/
private function assignGroups($link, string $username, string $userDn, array $provisioning): array
{
$list = trim((string) ($provisioning['member_of_list'] ?? ''));
$groupDns = $list !== ''
? $this->parseGroupDns($list)
: array_values(array_map('strval', (array) ($this->config['ldap']['default_groups'] ?? [])));
return $this->syncGroupsForUser($link, $username, $userDn, $this->resolveTargetGroupsFromProvisioning($provisioning));
}
/**
* @param resource|\LDAP\Connection $link
* @param array<int, string> $targetGroups
* @return array<int, string>
*/
private function syncGroupsForUser($link, string $username, string $userDn, array $targetGroups): array
{
$mode = (string) ($this->config['ldap']['group_assignment_mode'] ?? 'group_memberuid');
$updated = [];
if ($groupDns === [] || $mode === 'none') {
if ($targetGroups === [] || $mode === 'none') {
return [];
}
@@ -272,8 +395,22 @@ final class LdapProvisioner
$attribute = (string) ($this->config['ldap']['group_member_attribute'] ?? 'memberUid');
$value = $attribute === 'member' ? $userDn : $username;
$knownGroups = array_values(array_unique(array_filter(array_merge(
$targetGroups,
array_map(static fn (array $group): string => (string) ($group['dn'] ?? ''), (array) ($this->config['ldap']['available_groups'] ?? [])),
array_filter([(string) ($this->config['ldap']['pending_group_dn'] ?? '')]),
array_values(array_map('strval', (array) ($this->config['ldap']['default_groups'] ?? [])))
))));
foreach ($groupDns as $groupDn) {
foreach ($knownGroups as $groupDn) {
if (in_array($groupDn, $targetGroups, true)) {
continue;
}
@ldap_mod_del($link, $groupDn, [$attribute => [$value]]);
}
foreach ($targetGroups as $groupDn) {
$result = @ldap_mod_add($link, $groupDn, [$attribute => [$value]]);
if ($result === false) {
@@ -293,6 +430,21 @@ final class LdapProvisioner
return $updated;
}
/**
* @param array<string, mixed> $provisioning
* @return array<int, string>
*/
private function resolveTargetGroupsFromProvisioning(array $provisioning): array
{
$list = trim((string) ($provisioning['member_of_list'] ?? ''));
if ($list !== '') {
return $this->parseGroupDns($list);
}
return array_values(array_map('strval', (array) ($this->config['ldap']['default_groups'] ?? [])));
}
/**
* @return resource|\LDAP\Connection
*/

71
src/App/Mailer.php Normal file
View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace App;
final class Mailer
{
private JsonStore $logStore;
/**
* @param array<string, mixed> $config
*/
public function __construct(
string $projectRoot,
private readonly array $config,
) {
$logPath = $projectRoot . '/' . ltrim((string) ($this->config['mail']['log_path'] ?? 'data/mail.log'), '/');
$this->logStore = new JsonStore($logPath);
}
/**
* @return array{success: bool, message: string}
*/
public function send(string $toEmail, string $subject, string $body): array
{
$fromEmail = (string) ($this->config['mail']['from_email'] ?? '');
$fromName = (string) ($this->config['mail']['from_name'] ?? '');
$headers = [
'MIME-Version: 1.0',
'Content-Type: text/plain; charset=UTF-8',
];
if ($fromEmail !== '') {
$headers[] = 'From: ' . ($fromName !== '' ? sprintf('%s <%s>', $fromName, $fromEmail) : $fromEmail);
}
$success = false;
if ((bool) ($this->config['mail']['enabled'] ?? false) && function_exists('mail')) {
$success = @mail($toEmail, $subject, $body, implode("\r\n", $headers));
}
$logged = $this->logMail($toEmail, $subject, $body);
if ($success) {
return ['success' => true, 'message' => 'E-Mail wurde versendet.'];
}
return [
'success' => false,
'message' => $logged
? 'Mailversand war nicht erfolgreich. Die Nachricht wurde im Mail-Log abgelegt.'
: 'Mailversand war nicht erfolgreich.',
];
}
private function logMail(string $toEmail, string $subject, string $body): bool
{
$payload = $this->logStore->read();
$payload[] = [
'created_at' => gmdate('c'),
'to' => $toEmail,
'subject' => $subject,
'body' => $body,
];
$this->logStore->write($payload);
return true;
}
}

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace App;
final class PasswordResetService
{
private PasswordResetTokenStore $store;
private Mailer $mailer;
private LdapProvisioner $ldap;
/**
* @param array<string, mixed> $config
*/
public function __construct(
private readonly string $projectRoot,
private readonly array $config,
) {
$tokenPath = $this->projectRoot . '/' . ltrim((string) ($this->config['reset_token_path'] ?? 'data/password-reset-tokens.json'), '/');
$this->store = new PasswordResetTokenStore($tokenPath);
$this->mailer = new Mailer($projectRoot, $config);
$this->ldap = new LdapProvisioner($config);
}
/**
* @return array{success: bool, message: string}
*/
public function request(string $username, string $origin): array
{
$username = strtolower(trim($username));
if ($username === '') {
return ['success' => false, 'message' => 'Bitte einen Benutzernamen angeben.'];
}
$user = $this->ldap->getUserByUsername($username, ['mail', 'displayName', 'uid']);
if ($user === null) {
return ['success' => false, 'message' => 'Benutzer wurde im LDAP nicht gefunden.'];
}
$email = (string) ($user['mail'] ?? '');
if ($email === '') {
return ['success' => false, 'message' => 'Für diesen Benutzer ist keine E-Mail-Adresse im LDAP hinterlegt.'];
}
$token = bin2hex(random_bytes(24));
$expiresAt = gmdate('c', time() + ((int) ($this->config['reset_link_ttl_hours'] ?? 8) * 3600));
$this->store->add([
'token' => $token,
'username' => $username,
'email' => $email,
'status' => 'active',
'created_at' => gmdate('c'),
'expires_at' => $expiresAt,
]);
$link = rtrim($origin, '/') . '/auth/reset-password/?token=' . urlencode($token);
$body = "Hallo,\n\nfür dein Kusche.Berlin Desktop-Konto wurde ein Passwort-Reset angefordert.\n\nLink: {$link}\n\nDer Link ist bis {$expiresAt} gültig.\n\nWenn du das nicht warst, kannst du diese E-Mail ignorieren.\n";
$mail = $this->mailer->send($email, 'Passwort zurücksetzen', $body);
return $mail['success']
? ['success' => true, 'message' => 'Reset-Link wurde per E-Mail versendet.']
: ['success' => false, 'message' => $mail['message']];
}
/**
* @return array<string, mixed>|null
*/
public function validate(string $token): ?array
{
return $this->store->findActive($token);
}
/**
* @return array{success: bool, message: string}
*/
public function complete(string $token, string $password, string $passwordConfirm): array
{
$record = $this->validate($token);
if ($record === null) {
return ['success' => false, 'message' => 'Der Reset-Link ist ungültig oder abgelaufen.'];
}
if (strlen($password) < 12) {
return ['success' => false, 'message' => 'Das neue Passwort muss mindestens 12 Zeichen lang sein.'];
}
if ($password !== $passwordConfirm) {
return ['success' => false, 'message' => 'Die Passwörter stimmen nicht überein.'];
}
$result = $this->ldap->updateUserPassword((string) ($record['username'] ?? ''), $password);
if (!($result['success'] ?? false)) {
return ['success' => false, 'message' => (string) ($result['message'] ?? 'Passwortänderung fehlgeschlagen.')];
}
$this->store->deactivate($token, 'used');
return ['success' => true, 'message' => 'Das Passwort wurde erfolgreich zurückgesetzt.'];
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App;
final class PasswordResetTokenStore
{
private JsonStore $store;
public function __construct(string $storagePath)
{
$this->store = new JsonStore($storagePath);
}
/**
* @return array<int, array<string, mixed>>
*/
public function all(): array
{
$items = $this->store->read();
return array_values(array_filter($items, 'is_array'));
}
/**
* @param array<string, mixed> $record
*/
public function add(array $record): void
{
$items = $this->all();
$items[] = $record;
$this->store->write($items);
}
/**
* @return array<string, mixed>|null
*/
public function findActive(string $token): ?array
{
foreach ($this->all() as $item) {
if ((string) ($item['token'] ?? '') !== $token) {
continue;
}
if ((string) ($item['status'] ?? 'active') !== 'active') {
return null;
}
if (strtotime((string) ($item['expires_at'] ?? '')) < time()) {
return null;
}
return $item;
}
return null;
}
public function deactivate(string $token, string $status): void
{
$items = $this->all();
foreach ($items as $index => $item) {
if ((string) ($item['token'] ?? '') !== $token) {
continue;
}
$items[$index]['status'] = $status;
$items[$index]['used_at'] = gmdate('c');
break;
}
$this->store->write($items);
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App;
final class RegistrationIdentityAllocator
{
private JsonStore $store;
/**
* @param array<string, mixed> $config
*/
public function __construct(
string $storagePath,
private readonly array $config,
) {
$this->store = new JsonStore($storagePath);
}
/**
* @return array{uid_number: string, gid_number: string, samba_sid: string}
*/
public function allocate(): array
{
$payload = $this->store->read();
$nextUid = (int) ($payload['next_uid_number'] ?? ($this->config['ldap']['uid_number_start'] ?? 1000010));
$nextRid = (int) ($payload['next_samba_rid'] ?? ($this->config['ldap']['samba_rid_start'] ?? 1025));
$gidNumber = (int) ($this->config['ldap']['gid_number_default'] ?? 1000012);
$domainSid = trim((string) ($this->config['ldap']['samba_domain_sid'] ?? ''));
$payload['next_uid_number'] = $nextUid + 1;
$payload['next_samba_rid'] = $nextRid + 1;
$this->store->write($payload);
return [
'uid_number' => (string) $nextUid,
'gid_number' => (string) $gidNumber,
'samba_sid' => $domainSid !== '' ? $domainSid . '-' . $nextRid : '',
];
}
}

View File

@@ -10,6 +10,8 @@ final class RegistrationService
private RegistrationLdifBuilder $ldifBuilder;
private RegistrationSecretBox $secretBox;
private LdapProvisioner $ldapProvisioner;
private RegistrationIdentityAllocator $identityAllocator;
private PasswordResetService $passwordResetService;
/**
* @param array<string, mixed> $config
@@ -23,6 +25,9 @@ final class RegistrationService
$this->ldifBuilder = new RegistrationLdifBuilder($this->config);
$this->secretBox = new RegistrationSecretBox((string) ($this->config['password_crypto_key'] ?? ''));
$this->ldapProvisioner = new LdapProvisioner($this->config);
$counterPath = $this->resolveProjectPath((string) ($this->config['identity_counter_path'] ?? 'data/registration-identity-counter.json'));
$this->identityAllocator = new RegistrationIdentityAllocator($counterPath, $this->config);
$this->passwordResetService = new PasswordResetService($this->projectRoot, $this->config);
}
public function isEnabled(): bool
@@ -100,6 +105,10 @@ final class RegistrationService
$errors[] = 'Für diesen Benutzernamen oder diese E-Mail existiert bereits eine offene oder freigegebene Registrierung.';
}
if ($this->ldapProvisioner->usernameExists($username)) {
$errors[] = 'Dieser Benutzername ist bereits im LDAP vergeben.';
}
if ($errors !== []) {
return [
'success' => false,
@@ -107,9 +116,26 @@ final class RegistrationService
];
}
$identities = $this->identityAllocator->allocate();
$pendingGroup = trim((string) ($this->config['ldap']['pending_group_dn'] ?? ''));
$provisioning = [
'uid_number' => $identities['uid_number'],
'gid_number' => $identities['gid_number'],
'home_directory' => rtrim((string) ($this->config['ldap']['home_directory_prefix'] ?? '/home'), '/') . '/' . $username,
'login_shell' => trim((string) ($this->config['ldap']['default_login_shell'] ?? '/bin/sh')),
'member_of_list' => $pendingGroup,
'samba_sid' => $identities['samba_sid'],
'apple_generateduid' => '',
'display_name' => $displayName,
'cn' => $displayName !== '' ? $displayName : $username,
'gecos' => $displayName,
'admin_note' => '',
'ldap_created_at' => gmdate('c'),
];
$request = [
'id' => bin2hex(random_bytes(8)),
'status' => 'pending',
'status' => 'pending_approval',
'created_at' => gmdate('c'),
'updated_at' => gmdate('c'),
'username' => $username,
@@ -125,8 +151,29 @@ final class RegistrationService
'host' => (string) ($_SERVER['HTTP_HOST'] ?? ''),
],
'review' => null,
'provisioning' => null,
'provisioning' => $provisioning,
];
$passwordSecret = $this->secretBox->encrypt($password);
try {
$ldapResult = $this->ldapProvisioner->createInactiveUser($request, $provisioning, $password);
$request['password_secret'] = null;
$request['provisioning']['ldap_dn'] = $ldapResult['dn'];
$request['provisioning']['group_updates'] = $ldapResult['group_updates'];
$request['provisioning']['status'] = 'ldap_created_inactive';
$request['provisioning']['ldif_path'] = $this->writeLdifDraft($request['id'], $username, $this->ldifBuilder->build($request, $provisioning));
} catch (\Throwable $exception) {
$request['password_secret'] = $passwordSecret;
$request['status'] = 'provisioning_failed';
$request['provisioning']['status'] = 'provisioning_failed';
$request['provisioning']['error'] = $exception->getMessage();
return [
'success' => false,
'errors' => ['LDAP-Provisionierung bei Registrierung fehlgeschlagen: ' . $exception->getMessage()],
'request' => $this->store->create($request),
];
}
return [
'success' => true,
@@ -146,49 +193,45 @@ final class RegistrationService
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'] ?? ''));
$memberOfList = trim((string) ($input['member_of_list'] ?? ''));
$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 ($memberOfList === '') {
$errors[] = 'Für die Freischaltung muss mindestens eine Zielgruppe ausgewählt werden.';
}
if ($errors !== []) {
return ['success' => false, 'errors' => $errors];
}
$activate = $this->ldapProvisioner->activateUser((string) ($request['username'] ?? ''), preg_split('/\r\n|\r|\n/', $memberOfList) ?: []);
if (!($activate['success'] ?? false)) {
return ['success' => false, 'errors' => [(string) ($activate['message'] ?? 'Aktivierung fehlgeschlagen.')]];
}
$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'))),
'uid_number' => (string) ($request['provisioning']['uid_number'] ?? ''),
'gid_number' => (string) ($request['provisioning']['gid_number'] ?? ''),
'home_directory' => (string) ($request['provisioning']['home_directory'] ?? ''),
'login_shell' => (string) ($request['provisioning']['login_shell'] ?? ($this->config['ldap']['default_login_shell'] ?? '/bin/sh')),
'member_of_list' => $memberOfList,
'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'] ?? ''))),
'samba_sid' => (string) ($request['provisioning']['samba_sid'] ?? ''),
'apple_generateduid' => (string) ($request['provisioning']['apple_generateduid'] ?? ''),
'display_name' => (string) ($request['provisioning']['display_name'] ?? $request['display_name'] ?? ''),
'cn' => (string) ($request['provisioning']['cn'] ?? $request['display_name'] ?? $request['username'] ?? ''),
'gecos' => (string) ($request['provisioning']['gecos'] ?? $request['display_name'] ?? ''),
'admin_note' => $adminNote,
'ldif_generated_at' => gmdate('c'),
'ldap_activated_at' => gmdate('c'),
'ldap_dn' => (string) ($activate['dn'] ?? ($request['provisioning']['ldap_dn'] ?? '')),
'status' => 'approved_active',
];
$request = $this->store->update(
$id,
static function (array $item) use ($provisioning, $reviewer, $adminNote): array {
$item['status'] = 'approved_pending_provisioning';
$item['status'] = 'approved_active';
$item['updated_at'] = gmdate('c');
$item['review'] = [
'reviewed_at' => gmdate('c'),
@@ -206,54 +249,6 @@ final class RegistrationService
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);
try {
$passwordSecret = (string) ($request['password_secret'] ?? '');
if ($passwordSecret === '') {
throw new \RuntimeException('Für diese Registrierung ist kein Passwort mehr hinterlegt.');
}
$plainPassword = $this->secretBox->decrypt($passwordSecret);
$ldapResult = $this->ldapProvisioner->createInactiveUser($request, $provisioning, $plainPassword);
$request = $this->store->update(
$id,
static function (array $item) use ($ldifPath, $ldapResult): array {
$item['status'] = 'ldap_created_inactive';
$item['updated_at'] = gmdate('c');
$item['password_secret'] = null;
$item['provisioning']['ldif_path'] = $ldifPath;
$item['provisioning']['status'] = 'ldap_created_inactive';
$item['provisioning']['ldap_dn'] = $ldapResult['dn'];
$item['provisioning']['group_updates'] = $ldapResult['group_updates'];
return $item;
}
);
} catch (\Throwable $exception) {
$request = $this->store->update(
$id,
static function (array $item) use ($ldifPath, $exception): array {
$item['status'] = 'provisioning_failed';
$item['updated_at'] = gmdate('c');
$item['provisioning']['ldif_path'] = $ldifPath;
$item['provisioning']['status'] = 'provisioning_failed';
$item['provisioning']['error'] = $exception->getMessage();
return $item;
}
);
return [
'success' => false,
'errors' => ['LDAP-Provisionierung fehlgeschlagen: ' . $exception->getMessage()],
'request' => $request,
];
}
return [
'success' => true,
'request' => $request,
@@ -297,6 +292,66 @@ final class RegistrationService
];
}
/**
* @return array{success: bool, message: string}
*/
public function sendPasswordReset(string $username, string $origin): array
{
return $this->passwordResetService->request($username, $origin);
}
/**
* @return array{success: bool, message: string}
*/
public function deactivateUser(string $username): array
{
return $this->ldapProvisioner->deactivateUser($username);
}
/**
* @return array{success: bool, message: string}
*/
public function activateExistingUser(string $username, array $groupDns): array
{
return $this->ldapProvisioner->activateUser($username, $groupDns);
}
/**
* @return array{success: bool, message: string}
*/
public function setUserPassword(string $username, string $password): array
{
return $this->ldapProvisioner->updateUserPassword($username, $password);
}
/**
* @return array<int, array<string, mixed>>
*/
public function pendingApprovals(): array
{
return array_values(array_filter(
$this->all(),
static fn (array $item): bool => in_array((string) ($item['status'] ?? ''), ['pending_approval', 'provisioning_failed'], true)
));
}
/**
* @return array<int, array<string, string>>
*/
public function availableGroups(): array
{
return array_values(array_filter(
array_map(
static fn (array $group): array => [
'dn' => (string) ($group['dn'] ?? ''),
'label' => (string) ($group['label'] ?? ($group['dn'] ?? '')),
],
(array) ($this->config['ldap']['available_groups'] ?? [])
),
static fn (array $group): bool => $group['dn'] !== ''
));
}
private function writeLdifDraft(string $id, string $username, string $content): string
{
$directory = $this->resolveProjectPath((string) ($this->config['ldif_export_dir'] ?? 'data/registration-ldif'));