asdasd
This commit is contained in:
@@ -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
51
src/App/JsonStore.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
71
src/App/Mailer.php
Normal 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;
|
||||
}
|
||||
}
|
||||
106
src/App/PasswordResetService.php
Normal file
106
src/App/PasswordResetService.php
Normal 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.'];
|
||||
}
|
||||
}
|
||||
75
src/App/PasswordResetTokenStore.php
Normal file
75
src/App/PasswordResetTokenStore.php
Normal 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);
|
||||
}
|
||||
}
|
||||
42
src/App/RegistrationIdentityAllocator.php
Normal file
42
src/App/RegistrationIdentityAllocator.php
Normal 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 : '',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
|
||||
Reference in New Issue
Block a user