asdasd
This commit is contained in:
@@ -77,9 +77,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="kb-form-links">
|
<div class="kb-form-links">
|
||||||
<#if (url.loginResetCredentialsUrl!'')?has_content>
|
<a tabindex="6" id="kb-forgot-password-link" href="#" hidden>Passwort vergessen?</a>
|
||||||
<a tabindex="6" href="${url.loginResetCredentialsUrl}">Passwort vergessen?</a>
|
|
||||||
</#if>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -90,7 +88,7 @@
|
|||||||
|
|
||||||
<div id="kc-registration-container" class="kb-registration">
|
<div id="kc-registration-container" class="kb-registration">
|
||||||
<div id="kc-registration">
|
<div id="kc-registration">
|
||||||
<a class="kb-register-button" id="kb-register-button" tabindex="8" href="#" hidden>Registrieren</a>
|
<a class="kb-register-button" id="kb-register-button" tabindex="8" href="#" hidden>Neue Benutzer</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
(function () {
|
(function () {
|
||||||
const registerLink = document.getElementById('kb-register-button');
|
const registerLink = document.getElementById('kb-register-button');
|
||||||
|
const forgotPasswordLink = document.getElementById('kb-forgot-password-link');
|
||||||
|
|
||||||
if (!registerLink) {
|
if (!registerLink && !forgotPasswordLink) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,8 +15,14 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const target = new URL(redirectUri);
|
const target = new URL(redirectUri);
|
||||||
registerLink.href = target.origin + '/auth/register';
|
if (registerLink) {
|
||||||
registerLink.hidden = false;
|
registerLink.href = target.origin + '/auth/register/';
|
||||||
|
registerLink.hidden = false;
|
||||||
|
}
|
||||||
|
if (forgotPasswordLink) {
|
||||||
|
forgotPasswordLink.href = target.origin + '/auth/forgot-password/';
|
||||||
|
forgotPasswordLink.hidden = false;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Registrierungslink konnte nicht vorbereitet werden.', error);
|
console.warn('Registrierungslink konnte nicht vorbereitet werden.', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,21 @@ return [
|
|||||||
'module_name' => 'admin',
|
'module_name' => 'admin',
|
||||||
'summary' => 'Platzhalter fuer globale Verwaltung von Apps, Widgets, Integrationen und Suchmaschinen.',
|
'summary' => 'Platzhalter fuer globale Verwaltung von Apps, Widgets, Integrationen und Suchmaschinen.',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'app_id' => 'user-management',
|
||||||
|
'title' => 'User Management',
|
||||||
|
'icon' => 'UM',
|
||||||
|
'entry_route' => '/admin/users/',
|
||||||
|
'window_mode' => 'window',
|
||||||
|
'content_mode' => 'iframe',
|
||||||
|
'default_width' => 1180,
|
||||||
|
'default_height' => 760,
|
||||||
|
'supports_widget' => false,
|
||||||
|
'supports_tray' => true,
|
||||||
|
'module_name' => 'admin',
|
||||||
|
'required_groups' => ['administrators'],
|
||||||
|
'summary' => 'Benutzerverwaltung fuer Freischaltungen, Aktivierung, Deaktivierung und Passwort-Reset.',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'app_id' => 'desktop-login',
|
'app_id' => 'desktop-login',
|
||||||
'title' => 'Desktop Login',
|
'title' => 'Desktop Login',
|
||||||
|
|||||||
@@ -6,9 +6,18 @@ return [
|
|||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
'storage_path' => 'data/registration-requests.json',
|
'storage_path' => 'data/registration-requests.json',
|
||||||
'ldif_export_dir' => 'data/registration-ldif',
|
'ldif_export_dir' => 'data/registration-ldif',
|
||||||
|
'identity_counter_path' => 'data/registration-identity-counter.json',
|
||||||
|
'reset_token_path' => 'data/password-reset-tokens.json',
|
||||||
'approver_groups' => ['administrators', 'appadmin'],
|
'approver_groups' => ['administrators', 'appadmin'],
|
||||||
'approver_usernames' => [],
|
'approver_usernames' => [],
|
||||||
'password_crypto_key' => getenv('REGISTRATION_PASSWORD_KEY') ?: '',
|
'password_crypto_key' => getenv('REGISTRATION_PASSWORD_KEY') ?: '',
|
||||||
|
'reset_link_ttl_hours' => 8,
|
||||||
|
'mail' => [
|
||||||
|
'enabled' => true,
|
||||||
|
'from_email' => getenv('MAIL_FROM_EMAIL') ?: 'noreply@desktop.kusche.berlin',
|
||||||
|
'from_name' => getenv('MAIL_FROM_NAME') ?: 'Kusche.Berlin Desktop',
|
||||||
|
'log_path' => 'data/mail.log',
|
||||||
|
],
|
||||||
'ldap' => [
|
'ldap' => [
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
'host' => getenv('LDAP_HOST') ?: '127.0.0.1',
|
'host' => getenv('LDAP_HOST') ?: '127.0.0.1',
|
||||||
@@ -20,10 +29,24 @@ return [
|
|||||||
'default_groups' => [
|
'default_groups' => [
|
||||||
'cn=generaluser,cn=groups,dc=nas,dc=kusche,dc=berlin',
|
'cn=generaluser,cn=groups,dc=nas,dc=kusche,dc=berlin',
|
||||||
],
|
],
|
||||||
|
'pending_group_dn' => getenv('LDAP_PENDING_GROUP_DN') ?: 'cn=users-pending,cn=groups,dc=nas,dc=kusche,dc=berlin',
|
||||||
|
'available_groups' => [
|
||||||
|
['dn' => 'cn=administrators,cn=groups,dc=nas,dc=kusche,dc=berlin', 'label' => 'Administrators'],
|
||||||
|
['dn' => 'cn=appadmin,cn=groups,dc=nas,dc=kusche,dc=berlin', 'label' => 'App Admin'],
|
||||||
|
['dn' => 'cn=dockers,cn=groups,dc=nas,dc=kusche,dc=berlin', 'label' => 'Dockers'],
|
||||||
|
['dn' => 'cn=family,cn=groups,dc=nas,dc=kusche,dc=berlin', 'label' => 'Family'],
|
||||||
|
['dn' => 'cn=generaluser,cn=groups,dc=nas,dc=kusche,dc=berlin', 'label' => 'General User'],
|
||||||
|
['dn' => 'cn=internalfamily,cn=groups,dc=nas,dc=kusche,dc=berlin', 'label' => 'Internal Family'],
|
||||||
|
['dn' => 'cn=users,cn=groups,dc=nas,dc=kusche,dc=berlin', 'label' => 'Users'],
|
||||||
|
],
|
||||||
'group_assignment_mode' => getenv('LDAP_GROUP_ASSIGNMENT_MODE') ?: 'group_memberuid',
|
'group_assignment_mode' => getenv('LDAP_GROUP_ASSIGNMENT_MODE') ?: 'group_memberuid',
|
||||||
'group_member_attribute' => getenv('LDAP_GROUP_MEMBER_ATTRIBUTE') ?: 'memberUid',
|
'group_member_attribute' => getenv('LDAP_GROUP_MEMBER_ATTRIBUTE') ?: 'memberUid',
|
||||||
'default_login_shell' => '/bin/sh',
|
'default_login_shell' => '/bin/sh',
|
||||||
'home_directory_prefix' => '/home',
|
'home_directory_prefix' => '/home',
|
||||||
|
'uid_number_start' => (int) (getenv('LDAP_UID_NUMBER_START') ?: 1000010),
|
||||||
|
'gid_number_default' => (int) (getenv('LDAP_GID_NUMBER_DEFAULT') ?: 1000012),
|
||||||
|
'samba_domain_sid' => getenv('LDAP_SAMBA_DOMAIN_SID') ?: 'S-1-5-21-2039032496-2228230393-4197958046',
|
||||||
|
'samba_rid_start' => (int) (getenv('LDAP_SAMBA_RID_START') ?: 1025),
|
||||||
'shadow_expire' => 1,
|
'shadow_expire' => 1,
|
||||||
'active_shadow_expire' => -1,
|
'active_shadow_expire' => -1,
|
||||||
'shadow_flag' => 0,
|
'shadow_flag' => 0,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ $headline = (string) ($loginScreen['branding']['headline'] ?? 'Desktop Login');
|
|||||||
$subheadline = (string) ($loginScreen['branding']['subheadline'] ?? '');
|
$subheadline = (string) ($loginScreen['branding']['subheadline'] ?? '');
|
||||||
$loginUrl = $loginScreen['login_url'] ?? null;
|
$loginUrl = $loginScreen['login_url'] ?? null;
|
||||||
$registerUrl = $loginScreen['register_url'] ?? null;
|
$registerUrl = $loginScreen['register_url'] ?? null;
|
||||||
|
$forgotPasswordUrl = $loginScreen['forgot_password_url'] ?? null;
|
||||||
$previewUrl = $loginScreen['preview_url'] ?? null;
|
$previewUrl = $loginScreen['preview_url'] ?? null;
|
||||||
$keycloakReady = (bool) ($loginScreen['keycloak_ready'] ?? false);
|
$keycloakReady = (bool) ($loginScreen['keycloak_ready'] ?? false);
|
||||||
$callbackUrl = (string) ($loginScreen['callback_url'] ?? '');
|
$callbackUrl = (string) ($loginScreen['callback_url'] ?? '');
|
||||||
@@ -41,6 +42,10 @@ $callbackUrl = (string) ($loginScreen['callback_url'] ?? '');
|
|||||||
<?php if ($registerUrl !== null): ?>
|
<?php if ($registerUrl !== null): ?>
|
||||||
<a class="login-button login-button-secondary" href="<?= htmlspecialchars($registerUrl, ENT_QUOTES) ?>">Registrierung anfragen</a>
|
<a class="login-button login-button-secondary" href="<?= htmlspecialchars($registerUrl, ENT_QUOTES) ?>">Registrierung anfragen</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($forgotPasswordUrl !== null): ?>
|
||||||
|
<a class="login-button login-button-secondary" href="<?= htmlspecialchars($forgotPasswordUrl, ENT_QUOTES) ?>">Passwort vergessen</a>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<dl class="login-meta">
|
<dl class="login-meta">
|
||||||
|
|||||||
@@ -1,177 +1,3 @@
|
|||||||
<?php
|
<?php
|
||||||
|
http_response_code(404);
|
||||||
declare(strict_types=1);
|
echo 'LDAP-Test ist deaktiviert.';
|
||||||
|
|
||||||
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
|
|
||||||
|
|
||||||
use App\ConfigLoader;
|
|
||||||
use App\LdapProvisioner;
|
|
||||||
|
|
||||||
$projectRoot = dirname(__DIR__, 3);
|
|
||||||
$config = ConfigLoader::load($projectRoot, 'registration');
|
|
||||||
$ldap = new LdapProvisioner($config);
|
|
||||||
$diagnostics = $ldap->diagnostics();
|
|
||||||
|
|
||||||
$serviceResult = $ldap->testServiceBind();
|
|
||||||
$messages = [];
|
|
||||||
$errors = [];
|
|
||||||
$form = ['username' => '', 'password' => '', 'new_password' => ''];
|
|
||||||
$attempt = null;
|
|
||||||
$currentUserDn = null;
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$action = (string) ($_POST['action'] ?? '');
|
|
||||||
$form['username'] = trim((string) ($_POST['username'] ?? ''));
|
|
||||||
$form['password'] = (string) ($_POST['password'] ?? '');
|
|
||||||
$form['new_password'] = (string) ($_POST['new_password'] ?? '');
|
|
||||||
|
|
||||||
if ($form['username'] === '') {
|
|
||||||
$errors[] = 'Benutzername ist erforderlich.';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($form['password'] === '') {
|
|
||||||
$errors[] = 'Passwort ist erforderlich.';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($action === 'change-password' && strlen($form['new_password']) < 12) {
|
|
||||||
$errors[] = 'Das neue Passwort muss mindestens 12 Zeichen lang sein.';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($errors === []) {
|
|
||||||
$currentUserDn = $ldap->userDnForUsername($form['username']);
|
|
||||||
|
|
||||||
if ($action === 'test-login') {
|
|
||||||
$attempt = [
|
|
||||||
'action' => 'Login-Test',
|
|
||||||
'username' => $form['username'],
|
|
||||||
'user_dn' => $currentUserDn,
|
|
||||||
];
|
|
||||||
$result = $ldap->testUserCredentials($form['username'], $form['password']);
|
|
||||||
|
|
||||||
if ($result['success'] ?? false) {
|
|
||||||
$messages[] = $result['message'] . ' DN: ' . (string) ($result['dn'] ?? '');
|
|
||||||
} else {
|
|
||||||
$errors[] = (string) ($result['message'] ?? 'LDAP-Login fehlgeschlagen.');
|
|
||||||
}
|
|
||||||
} elseif ($action === 'change-password') {
|
|
||||||
$attempt = [
|
|
||||||
'action' => 'Passwortänderung',
|
|
||||||
'username' => $form['username'],
|
|
||||||
'user_dn' => $currentUserDn,
|
|
||||||
'new_password_length' => (string) strlen($form['new_password']),
|
|
||||||
];
|
|
||||||
$loginCheck = $ldap->testUserCredentials($form['username'], $form['password']);
|
|
||||||
|
|
||||||
if (!(bool) ($loginCheck['success'] ?? false)) {
|
|
||||||
$errors[] = 'Der Login mit aktuellem Passwort ist fehlgeschlagen: ' . (string) ($loginCheck['message'] ?? '');
|
|
||||||
} else {
|
|
||||||
$result = $ldap->updateUserPassword($form['username'], $form['new_password']);
|
|
||||||
|
|
||||||
if ($result['success'] ?? false) {
|
|
||||||
$messages[] = $result['message'] . ' DN: ' . (string) ($result['dn'] ?? '');
|
|
||||||
$form['password'] = '';
|
|
||||||
$form['new_password'] = '';
|
|
||||||
} else {
|
|
||||||
$errors[] = (string) ($result['message'] ?? 'Passwortänderung fehlgeschlagen.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?><!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>LDAP-Test</title>
|
|
||||||
<link rel="stylesheet" href="/assets/auth/login.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main class="login-shell">
|
|
||||||
<section class="login-panel login-panel-wide">
|
|
||||||
<div class="login-panel-top">
|
|
||||||
<p class="login-kicker">Kusche.Berlin</p>
|
|
||||||
<h1>LDAP-Test</h1>
|
|
||||||
<p class="login-copy">Diese Testseite läuft unabhängig vom eigentlichen Desktop-Login. Hier kannst du nur LDAP-Login und Passwortänderung inklusive Samba-Feldern prüfen.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="login-notice <?= ($serviceResult['success'] ?? false) ? 'login-notice-success' : 'login-notice-error' ?>">
|
|
||||||
<strong>Service-Bind</strong>
|
|
||||||
<p><?= htmlspecialchars((string) ($serviceResult['message'] ?? ''), ENT_QUOTES) ?></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="login-notice login-notice-info">
|
|
||||||
<strong>Verbindungsdaten</strong>
|
|
||||||
<ul class="login-list">
|
|
||||||
<li>Host: <?= htmlspecialchars((string) ($diagnostics['host'] ?? ''), ENT_QUOTES) ?></li>
|
|
||||||
<li>Port: <?= htmlspecialchars((string) ($diagnostics['port'] ?? ''), ENT_QUOTES) ?></li>
|
|
||||||
<li>Bind-DN: <?= htmlspecialchars((string) ($diagnostics['bind_dn'] ?? ''), ENT_QUOTES) ?></li>
|
|
||||||
<li>Users-DN: <?= htmlspecialchars((string) ($diagnostics['users_dn'] ?? ''), ENT_QUOTES) ?></li>
|
|
||||||
<li>StartTLS: <?= htmlspecialchars((string) ($diagnostics['starttls'] ?? ''), ENT_QUOTES) ?></li>
|
|
||||||
<li>Gruppenmodus: <?= htmlspecialchars((string) ($diagnostics['group_assignment_mode'] ?? ''), ENT_QUOTES) ?></li>
|
|
||||||
<li>Gruppenattribut: <?= htmlspecialchars((string) ($diagnostics['group_member_attribute'] ?? ''), ENT_QUOTES) ?></li>
|
|
||||||
<li>PHP LDAP-Erweiterung: <?= htmlspecialchars((string) ($diagnostics['ldap_extension'] ?? ''), ENT_QUOTES) ?></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($messages !== []): ?>
|
|
||||||
<div class="login-notice login-notice-success">
|
|
||||||
<strong>Ergebnis</strong>
|
|
||||||
<ul class="login-list">
|
|
||||||
<?php foreach ($messages as $message): ?>
|
|
||||||
<li><?= htmlspecialchars($message, ENT_QUOTES) ?></li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($errors !== []): ?>
|
|
||||||
<div class="login-notice login-notice-error">
|
|
||||||
<strong>Bitte prüfen</strong>
|
|
||||||
<ul class="login-list">
|
|
||||||
<?php foreach ($errors as $error): ?>
|
|
||||||
<li><?= htmlspecialchars($error, ENT_QUOTES) ?></li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (is_array($attempt)): ?>
|
|
||||||
<div class="login-notice login-notice-info">
|
|
||||||
<strong>Letzter Versuch</strong>
|
|
||||||
<ul class="login-list">
|
|
||||||
<li>Aktion: <?= htmlspecialchars((string) ($attempt['action'] ?? ''), ENT_QUOTES) ?></li>
|
|
||||||
<li>Benutzername: <?= htmlspecialchars((string) ($attempt['username'] ?? ''), ENT_QUOTES) ?></li>
|
|
||||||
<li>User-DN: <?= htmlspecialchars((string) ($attempt['user_dn'] ?? ''), ENT_QUOTES) ?></li>
|
|
||||||
<?php if (isset($attempt['new_password_length'])): ?>
|
|
||||||
<li>Länge neues Passwort: <?= htmlspecialchars((string) ($attempt['new_password_length'] ?? ''), ENT_QUOTES) ?></li>
|
|
||||||
<?php endif; ?>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<form class="login-form" method="post" action="/admin/ldap-test/">
|
|
||||||
<label class="login-field">
|
|
||||||
<span>Benutzername</span>
|
|
||||||
<input type="text" name="username" value="<?= htmlspecialchars($form['username'], ENT_QUOTES) ?>" required>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="login-field">
|
|
||||||
<span>Passwort</span>
|
|
||||||
<input type="password" name="password" value="" required>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="login-field">
|
|
||||||
<span>Neues Passwort</span>
|
|
||||||
<input type="password" name="new_password" value="" placeholder="Nur für Passwortänderung erforderlich">
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="login-actions">
|
|
||||||
<button class="login-button login-button-primary" type="submit" name="action" value="test-login">Login-Test</button>
|
|
||||||
<button class="login-button login-button-secondary" type="submit" name="action" value="change-password">Neues Passwort setzen</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
234
public/admin/users/index.php
Normal file
234
public/admin/users/index.php
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
|
||||||
|
|
||||||
|
use App\ConfigLoader;
|
||||||
|
use App\RegistrationService;
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$projectRoot = dirname(__DIR__, 3);
|
||||||
|
$registration = new RegistrationService($projectRoot, ConfigLoader::load($projectRoot, 'registration'));
|
||||||
|
$currentUser = is_array($_SESSION['desktop_auth']['user'] ?? null) ? $_SESSION['desktop_auth']['user'] : [];
|
||||||
|
$currentGroups = array_map(static fn (string $group): string => strtolower(trim($group, '/')), array_values(array_map('strval', (array) ($currentUser['groups'] ?? []))));
|
||||||
|
|
||||||
|
if (!in_array('administrators', $currentGroups, true)) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo 'Kein Zugriff auf User Management.';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$csrfToken = (string) ($_SESSION['user_management_token'] ?? '');
|
||||||
|
|
||||||
|
if ($csrfToken === '') {
|
||||||
|
$csrfToken = bin2hex(random_bytes(16));
|
||||||
|
$_SESSION['user_management_token'] = $csrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
$messages = [];
|
||||||
|
$errors = [];
|
||||||
|
$selectedId = trim((string) ($_GET['id'] ?? $_POST['selected_id'] ?? ''));
|
||||||
|
$manualUsername = trim((string) ($_POST['manual_username'] ?? ''));
|
||||||
|
$manualResetUsername = trim((string) ($_POST['reset_username'] ?? ''));
|
||||||
|
$manualResetPassword = (string) ($_POST['reset_password'] ?? '');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$postedToken = (string) ($_POST['csrf_token'] ?? '');
|
||||||
|
|
||||||
|
if ($postedToken === '' || !hash_equals($csrfToken, $postedToken)) {
|
||||||
|
$errors[] = 'Die Formularprüfung ist fehlgeschlagen.';
|
||||||
|
} else {
|
||||||
|
$action = (string) ($_POST['action'] ?? '');
|
||||||
|
|
||||||
|
if ($action === 'approve-registration') {
|
||||||
|
$groupDns = array_values(array_filter(array_map('strval', (array) ($_POST['group_dns'] ?? []))));
|
||||||
|
$memberOfList = implode(PHP_EOL, $groupDns);
|
||||||
|
$result = $registration->approve($selectedId, [
|
||||||
|
'member_of_list' => $memberOfList,
|
||||||
|
'admin_note' => (string) ($_POST['admin_note'] ?? ''),
|
||||||
|
], (string) ($currentUser['username'] ?? 'administrator'));
|
||||||
|
|
||||||
|
if (($result['success'] ?? false) !== true) {
|
||||||
|
$errors = array_merge($errors, array_values(array_map('strval', (array) ($result['errors'] ?? []))));
|
||||||
|
} else {
|
||||||
|
$messages[] = 'Registrierung wurde freigeschaltet.';
|
||||||
|
}
|
||||||
|
} elseif ($action === 'deactivate-user') {
|
||||||
|
$result = $registration->deactivateUser($manualUsername);
|
||||||
|
($result['success'] ?? false) ? $messages[] = (string) ($result['message'] ?? '') : $errors[] = (string) ($result['message'] ?? '');
|
||||||
|
} elseif ($action === 'activate-user') {
|
||||||
|
$groupDns = array_values(array_filter(array_map('strval', (array) ($_POST['manual_group_dns'] ?? []))));
|
||||||
|
$result = $registration->activateExistingUser($manualUsername, $groupDns);
|
||||||
|
($result['success'] ?? false) ? $messages[] = (string) ($result['message'] ?? '') : $errors[] = (string) ($result['message'] ?? '');
|
||||||
|
} elseif ($action === 'send-reset-mail') {
|
||||||
|
$origin = ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost');
|
||||||
|
$result = $registration->sendPasswordReset($manualResetUsername, $origin);
|
||||||
|
($result['success'] ?? false) ? $messages[] = (string) ($result['message'] ?? '') : $errors[] = (string) ($result['message'] ?? '');
|
||||||
|
} elseif ($action === 'set-password') {
|
||||||
|
if (strlen($manualResetPassword) < 12) {
|
||||||
|
$errors[] = 'Das neue Passwort muss mindestens 12 Zeichen lang sein.';
|
||||||
|
} else {
|
||||||
|
$result = $registration->setUserPassword($manualResetUsername, $manualResetPassword);
|
||||||
|
($result['success'] ?? false) ? $messages[] = (string) ($result['message'] ?? '') : $errors[] = (string) ($result['message'] ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pendingRequests = $registration->pendingApprovals();
|
||||||
|
$availableGroups = $registration->availableGroups();
|
||||||
|
$selected = $selectedId !== ''
|
||||||
|
? $registration->find($selectedId)
|
||||||
|
: ($pendingRequests[0] ?? null);
|
||||||
|
?><!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>User Management</title>
|
||||||
|
<link rel="stylesheet" href="/assets/auth/login.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="login-shell">
|
||||||
|
<section class="login-panel login-panel-admin">
|
||||||
|
<div class="login-panel-top">
|
||||||
|
<p class="login-kicker">Kusche.Berlin</p>
|
||||||
|
<h1>User Management</h1>
|
||||||
|
<p class="login-copy">Nur für Benutzer in der Gruppe <strong>administrators</strong>. Hier werden Neuregistrierungen freigeschaltet sowie bestehende LDAP-Benutzer aktiviert, deaktiviert oder zurückgesetzt.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($messages !== []): ?>
|
||||||
|
<div class="login-notice login-notice-success">
|
||||||
|
<strong>Ergebnis</strong>
|
||||||
|
<ul class="login-list">
|
||||||
|
<?php foreach ($messages as $message): ?>
|
||||||
|
<li><?= htmlspecialchars($message, ENT_QUOTES) ?></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($errors !== []): ?>
|
||||||
|
<div class="login-notice login-notice-error">
|
||||||
|
<strong>Bitte prüfen</strong>
|
||||||
|
<ul class="login-list">
|
||||||
|
<?php foreach ($errors as $error): ?>
|
||||||
|
<li><?= htmlspecialchars($error, ENT_QUOTES) ?></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="admin-layout">
|
||||||
|
<aside class="admin-sidebar">
|
||||||
|
<h2>Offene Registrierungen</h2>
|
||||||
|
<div class="admin-request-list">
|
||||||
|
<?php foreach ($pendingRequests as $request): ?>
|
||||||
|
<a class="admin-request-card<?= (($selected['id'] ?? '') === ($request['id'] ?? '')) ? ' is-active' : '' ?>" href="/admin/users/?id=<?= urlencode((string) ($request['id'] ?? '')) ?>">
|
||||||
|
<strong><?= htmlspecialchars((string) ($request['username'] ?? ''), ENT_QUOTES) ?></strong>
|
||||||
|
<span><?= htmlspecialchars((string) ($request['email'] ?? ''), ENT_QUOTES) ?></span>
|
||||||
|
<span class="admin-status"><?= htmlspecialchars((string) ($request['status'] ?? ''), ENT_QUOTES) ?></span>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section class="admin-detail">
|
||||||
|
<?php if (is_array($selected)): ?>
|
||||||
|
<div class="admin-summary">
|
||||||
|
<h2>Freischaltung</h2>
|
||||||
|
<p>Benutzer: <?= htmlspecialchars((string) ($selected['username'] ?? ''), ENT_QUOTES) ?></p>
|
||||||
|
<p>E-Mail: <?= htmlspecialchars((string) ($selected['email'] ?? ''), ENT_QUOTES) ?></p>
|
||||||
|
<p>Status: <?= htmlspecialchars((string) ($selected['status'] ?? ''), ENT_QUOTES) ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="login-form" method="post" action="/admin/users/">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES) ?>">
|
||||||
|
<input type="hidden" name="selected_id" value="<?= htmlspecialchars((string) ($selected['id'] ?? ''), ENT_QUOTES) ?>">
|
||||||
|
<input type="hidden" name="action" value="approve-registration">
|
||||||
|
|
||||||
|
<label class="login-field">
|
||||||
|
<span>Zielgruppen für Freischaltung</span>
|
||||||
|
<div class="admin-request-list">
|
||||||
|
<?php foreach ($availableGroups as $group): ?>
|
||||||
|
<label class="login-field">
|
||||||
|
<span>
|
||||||
|
<input type="checkbox" name="group_dns[]" value="<?= htmlspecialchars($group['dn'], ENT_QUOTES) ?>">
|
||||||
|
<?= htmlspecialchars($group['label'], ENT_QUOTES) ?>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="login-field">
|
||||||
|
<span>Admin-Notiz</span>
|
||||||
|
<textarea name="admin_note" rows="3"></textarea>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="login-actions">
|
||||||
|
<button class="login-button login-button-primary" type="submit">Freischaltung bestätigen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="admin-summary">
|
||||||
|
<h2>Bestehenden Benutzer verwalten</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="login-form" method="post" action="/admin/users/">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES) ?>">
|
||||||
|
<label class="login-field">
|
||||||
|
<span>Benutzername</span>
|
||||||
|
<input type="text" name="manual_username" value="<?= htmlspecialchars($manualUsername, ENT_QUOTES) ?>" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="login-field">
|
||||||
|
<span>Gruppen für Aktivierung</span>
|
||||||
|
<div class="admin-request-list">
|
||||||
|
<?php foreach ($availableGroups as $group): ?>
|
||||||
|
<label class="login-field">
|
||||||
|
<span>
|
||||||
|
<input type="checkbox" name="manual_group_dns[]" value="<?= htmlspecialchars($group['dn'], ENT_QUOTES) ?>">
|
||||||
|
<?= htmlspecialchars($group['label'], ENT_QUOTES) ?>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="login-actions">
|
||||||
|
<button class="login-button login-button-primary" type="submit" name="action" value="activate-user">Benutzer aktivieren</button>
|
||||||
|
<button class="login-button login-button-danger" type="submit" name="action" value="deactivate-user">Benutzer deaktivieren</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="admin-summary">
|
||||||
|
<h2>Passwort zurücksetzen</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="login-form" method="post" action="/admin/users/">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES) ?>">
|
||||||
|
<label class="login-field">
|
||||||
|
<span>Benutzername</span>
|
||||||
|
<input type="text" name="reset_username" value="<?= htmlspecialchars($manualResetUsername, ENT_QUOTES) ?>" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="login-field">
|
||||||
|
<span>Neues Passwort für direkten Reset</span>
|
||||||
|
<input type="password" name="reset_password" value="" placeholder="Optional für direkten Admin-Reset">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="login-actions">
|
||||||
|
<button class="login-button login-button-secondary" type="submit" name="action" value="send-reset-mail">Reset-Mail senden</button>
|
||||||
|
<button class="login-button login-button-primary" type="submit" name="action" value="set-password">Passwort direkt setzen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
66
public/auth/forgot-password/index.php
Normal file
66
public/auth/forgot-password/index.php
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
|
||||||
|
|
||||||
|
use App\ConfigLoader;
|
||||||
|
use App\RegistrationService;
|
||||||
|
|
||||||
|
$projectRoot = dirname(__DIR__, 3);
|
||||||
|
$registration = new RegistrationService($projectRoot, ConfigLoader::load($projectRoot, 'registration'));
|
||||||
|
$message = null;
|
||||||
|
$error = null;
|
||||||
|
$username = '';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$username = trim((string) ($_POST['username'] ?? ''));
|
||||||
|
$origin = ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost');
|
||||||
|
$result = $registration->sendPasswordReset($username, $origin);
|
||||||
|
|
||||||
|
if ($result['success'] ?? false) {
|
||||||
|
$message = (string) ($result['message'] ?? '');
|
||||||
|
} else {
|
||||||
|
$error = (string) ($result['message'] ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?><!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Passwort vergessen</title>
|
||||||
|
<link rel="stylesheet" href="/assets/auth/login.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="login-shell">
|
||||||
|
<section class="login-panel">
|
||||||
|
<div class="login-panel-top">
|
||||||
|
<p class="login-kicker">Kusche.Berlin</p>
|
||||||
|
<h1>Passwort vergessen</h1>
|
||||||
|
<p class="login-copy">Gib deinen Benutzernamen an. Wenn der LDAP-Benutzer eine E-Mail-Adresse hat, senden wir einen 8 Stunden gültigen Reset-Link.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($message !== null): ?>
|
||||||
|
<div class="login-notice login-notice-success"><p><?= htmlspecialchars($message, ENT_QUOTES) ?></p></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($error !== null): ?>
|
||||||
|
<div class="login-notice login-notice-error"><p><?= htmlspecialchars($error, ENT_QUOTES) ?></p></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form class="login-form" method="post" action="/auth/forgot-password/">
|
||||||
|
<label class="login-field">
|
||||||
|
<span>Benutzername</span>
|
||||||
|
<input type="text" name="username" value="<?= htmlspecialchars($username, ENT_QUOTES) ?>" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="login-actions">
|
||||||
|
<button class="login-button login-button-primary" type="submit">Reset-Link senden</button>
|
||||||
|
<a class="login-button login-button-secondary" href="/auth/login">Zurück zum Login</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -19,6 +19,7 @@ $loginScreen = [
|
|||||||
'branding' => $auth->branding(),
|
'branding' => $auth->branding(),
|
||||||
'login_url' => $auth->isConfigured() ? '/auth/keycloak' : null,
|
'login_url' => $auth->isConfigured() ? '/auth/keycloak' : null,
|
||||||
'register_url' => '/auth/register',
|
'register_url' => '/auth/register',
|
||||||
|
'forgot_password_url' => '/auth/forgot-password/',
|
||||||
'preview_url' => $previewUrl,
|
'preview_url' => $previewUrl,
|
||||||
'keycloak_ready' => $auth->isConfigured(),
|
'keycloak_ready' => $auth->isConfigured(),
|
||||||
'callback_url' => $auth->redirectUri(),
|
'callback_url' => $auth->redirectUri(),
|
||||||
|
|||||||
79
public/auth/reset-password/index.php
Normal file
79
public/auth/reset-password/index.php
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
|
||||||
|
|
||||||
|
use App\ConfigLoader;
|
||||||
|
use App\PasswordResetService;
|
||||||
|
|
||||||
|
$projectRoot = dirname(__DIR__, 3);
|
||||||
|
$service = new PasswordResetService($projectRoot, ConfigLoader::load($projectRoot, 'registration'));
|
||||||
|
$token = trim((string) ($_GET['token'] ?? $_POST['token'] ?? ''));
|
||||||
|
$record = $service->validate($token);
|
||||||
|
$message = null;
|
||||||
|
$error = null;
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$result = $service->complete(
|
||||||
|
$token,
|
||||||
|
(string) ($_POST['password'] ?? ''),
|
||||||
|
(string) ($_POST['password_confirm'] ?? '')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result['success'] ?? false) {
|
||||||
|
$message = (string) ($result['message'] ?? '');
|
||||||
|
$record = null;
|
||||||
|
} else {
|
||||||
|
$error = (string) ($result['message'] ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?><!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Passwort zurücksetzen</title>
|
||||||
|
<link rel="stylesheet" href="/assets/auth/login.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="login-shell">
|
||||||
|
<section class="login-panel">
|
||||||
|
<div class="login-panel-top">
|
||||||
|
<p class="login-kicker">Kusche.Berlin</p>
|
||||||
|
<h1>Passwort zurücksetzen</h1>
|
||||||
|
<p class="login-copy">Der Link ist 8 Stunden gültig und setzt sowohl LDAP- als auch Samba-Passwort neu.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($message !== null): ?>
|
||||||
|
<div class="login-notice login-notice-success"><p><?= htmlspecialchars($message, ENT_QUOTES) ?></p></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($error !== null): ?>
|
||||||
|
<div class="login-notice login-notice-error"><p><?= htmlspecialchars($error, ENT_QUOTES) ?></p></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($record !== null): ?>
|
||||||
|
<form class="login-form" method="post" action="/auth/reset-password/">
|
||||||
|
<input type="hidden" name="token" value="<?= htmlspecialchars($token, ENT_QUOTES) ?>">
|
||||||
|
<label class="login-field">
|
||||||
|
<span>Neues Passwort</span>
|
||||||
|
<input type="password" name="password" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="login-field">
|
||||||
|
<span>Neues Passwort wiederholen</span>
|
||||||
|
<input type="password" name="password_confirm" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="login-actions">
|
||||||
|
<button class="login-button login-button-primary" type="submit">Passwort zurücksetzen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="login-notice login-notice-info"><p>Der Token ist ungültig oder bereits verwendet.</p></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -29,13 +29,15 @@ final class App
|
|||||||
$widgetRegistry = new WidgetRegistry($this->projectRoot . '/config/widgets.php');
|
$widgetRegistry = new WidgetRegistry($this->projectRoot . '/config/widgets.php');
|
||||||
$skins = SkinResolver::all();
|
$skins = SkinResolver::all();
|
||||||
$activeSkin = SkinResolver::resolve($_GET['skin'] ?? null);
|
$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']);
|
$isAuthenticated = isset($_SESSION['desktop_auth']) && is_array($_SESSION['desktop_auth']);
|
||||||
$authUser = $isAuthenticated && is_array($_SESSION['desktop_auth']['user'] ?? null)
|
$authUser = $isAuthenticated && is_array($_SESSION['desktop_auth']['user'] ?? null)
|
||||||
? $_SESSION['desktop_auth']['user']
|
? $_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');
|
$displayName = (string) ($authUser['name'] ?? $authUser['username'] ?? 'Gast');
|
||||||
|
|
||||||
return [
|
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;
|
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}
|
* @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));
|
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 [
|
return [
|
||||||
'dn' => $userDn,
|
'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> $request
|
||||||
* @param array<string, mixed> $provisioning
|
* @param array<string, mixed> $provisioning
|
||||||
@@ -255,14 +372,20 @@ final class LdapProvisioner
|
|||||||
*/
|
*/
|
||||||
private function assignGroups($link, string $username, string $userDn, array $provisioning): array
|
private function assignGroups($link, string $username, string $userDn, array $provisioning): array
|
||||||
{
|
{
|
||||||
$list = trim((string) ($provisioning['member_of_list'] ?? ''));
|
return $this->syncGroupsForUser($link, $username, $userDn, $this->resolveTargetGroupsFromProvisioning($provisioning));
|
||||||
$groupDns = $list !== ''
|
}
|
||||||
? $this->parseGroupDns($list)
|
|
||||||
: array_values(array_map('strval', (array) ($this->config['ldap']['default_groups'] ?? [])));
|
/**
|
||||||
|
* @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');
|
$mode = (string) ($this->config['ldap']['group_assignment_mode'] ?? 'group_memberuid');
|
||||||
$updated = [];
|
$updated = [];
|
||||||
|
|
||||||
if ($groupDns === [] || $mode === 'none') {
|
if ($targetGroups === [] || $mode === 'none') {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,8 +395,22 @@ final class LdapProvisioner
|
|||||||
|
|
||||||
$attribute = (string) ($this->config['ldap']['group_member_attribute'] ?? 'memberUid');
|
$attribute = (string) ($this->config['ldap']['group_member_attribute'] ?? 'memberUid');
|
||||||
$value = $attribute === 'member' ? $userDn : $username;
|
$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]]);
|
$result = @ldap_mod_add($link, $groupDn, [$attribute => [$value]]);
|
||||||
|
|
||||||
if ($result === false) {
|
if ($result === false) {
|
||||||
@@ -293,6 +430,21 @@ final class LdapProvisioner
|
|||||||
return $updated;
|
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
|
* @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 RegistrationLdifBuilder $ldifBuilder;
|
||||||
private RegistrationSecretBox $secretBox;
|
private RegistrationSecretBox $secretBox;
|
||||||
private LdapProvisioner $ldapProvisioner;
|
private LdapProvisioner $ldapProvisioner;
|
||||||
|
private RegistrationIdentityAllocator $identityAllocator;
|
||||||
|
private PasswordResetService $passwordResetService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $config
|
* @param array<string, mixed> $config
|
||||||
@@ -23,6 +25,9 @@ final class RegistrationService
|
|||||||
$this->ldifBuilder = new RegistrationLdifBuilder($this->config);
|
$this->ldifBuilder = new RegistrationLdifBuilder($this->config);
|
||||||
$this->secretBox = new RegistrationSecretBox((string) ($this->config['password_crypto_key'] ?? ''));
|
$this->secretBox = new RegistrationSecretBox((string) ($this->config['password_crypto_key'] ?? ''));
|
||||||
$this->ldapProvisioner = new LdapProvisioner($this->config);
|
$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
|
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.';
|
$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 !== []) {
|
if ($errors !== []) {
|
||||||
return [
|
return [
|
||||||
'success' => false,
|
'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 = [
|
$request = [
|
||||||
'id' => bin2hex(random_bytes(8)),
|
'id' => bin2hex(random_bytes(8)),
|
||||||
'status' => 'pending',
|
'status' => 'pending_approval',
|
||||||
'created_at' => gmdate('c'),
|
'created_at' => gmdate('c'),
|
||||||
'updated_at' => gmdate('c'),
|
'updated_at' => gmdate('c'),
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
@@ -125,8 +151,29 @@ final class RegistrationService
|
|||||||
'host' => (string) ($_SERVER['HTTP_HOST'] ?? ''),
|
'host' => (string) ($_SERVER['HTTP_HOST'] ?? ''),
|
||||||
],
|
],
|
||||||
'review' => null,
|
'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 [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
@@ -146,49 +193,45 @@ final class RegistrationService
|
|||||||
return ['success' => false, 'errors' => ['Die Registrierung wurde nicht gefunden.']];
|
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'] ?? ''));
|
$memberOfList = trim((string) ($input['member_of_list'] ?? ''));
|
||||||
$adminNote = trim((string) ($input['admin_note'] ?? ''));
|
$adminNote = trim((string) ($input['admin_note'] ?? ''));
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
|
||||||
if ($uidNumber === '' || !ctype_digit($uidNumber)) {
|
if ($memberOfList === '') {
|
||||||
$errors[] = 'uidNumber ist erforderlich und muss numerisch sein.';
|
$errors[] = 'Für die Freischaltung muss mindestens eine Zielgruppe ausgewählt werden.';
|
||||||
}
|
|
||||||
|
|
||||||
if ($gidNumber === '' || !ctype_digit($gidNumber)) {
|
|
||||||
$errors[] = 'gidNumber ist erforderlich und muss numerisch sein.';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($homeDirectory === '') {
|
|
||||||
$errors[] = 'homeDirectory ist erforderlich.';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($errors !== []) {
|
if ($errors !== []) {
|
||||||
return ['success' => false, 'errors' => $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 = [
|
$provisioning = [
|
||||||
'uid_number' => $uidNumber,
|
'uid_number' => (string) ($request['provisioning']['uid_number'] ?? ''),
|
||||||
'gid_number' => $gidNumber,
|
'gid_number' => (string) ($request['provisioning']['gid_number'] ?? ''),
|
||||||
'home_directory' => $homeDirectory,
|
'home_directory' => (string) ($request['provisioning']['home_directory'] ?? ''),
|
||||||
'login_shell' => trim((string) ($input['login_shell'] ?? ($this->config['ldap']['default_login_shell'] ?? '/bin/sh'))),
|
'login_shell' => (string) ($request['provisioning']['login_shell'] ?? ($this->config['ldap']['default_login_shell'] ?? '/bin/sh')),
|
||||||
'member_of_list' => $memberOfList,
|
'member_of_list' => $memberOfList,
|
||||||
'samba_sid' => $sambaSid,
|
'samba_sid' => (string) ($request['provisioning']['samba_sid'] ?? ''),
|
||||||
'apple_generateduid' => trim((string) ($input['apple_generateduid'] ?? '')),
|
'apple_generateduid' => (string) ($request['provisioning']['apple_generateduid'] ?? ''),
|
||||||
'display_name' => trim((string) ($input['display_name'] ?? (string) ($request['display_name'] ?? ''))),
|
'display_name' => (string) ($request['provisioning']['display_name'] ?? $request['display_name'] ?? ''),
|
||||||
'cn' => trim((string) ($input['cn'] ?? (string) ($request['display_name'] ?? $request['username'] ?? ''))),
|
'cn' => (string) ($request['provisioning']['cn'] ?? $request['display_name'] ?? $request['username'] ?? ''),
|
||||||
'gecos' => trim((string) ($input['gecos'] ?? (string) ($request['display_name'] ?? ''))),
|
'gecos' => (string) ($request['provisioning']['gecos'] ?? $request['display_name'] ?? ''),
|
||||||
'admin_note' => $adminNote,
|
'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(
|
$request = $this->store->update(
|
||||||
$id,
|
$id,
|
||||||
static function (array $item) use ($provisioning, $reviewer, $adminNote): array {
|
static function (array $item) use ($provisioning, $reviewer, $adminNote): array {
|
||||||
$item['status'] = 'approved_pending_provisioning';
|
$item['status'] = 'approved_active';
|
||||||
$item['updated_at'] = gmdate('c');
|
$item['updated_at'] = gmdate('c');
|
||||||
$item['review'] = [
|
$item['review'] = [
|
||||||
'reviewed_at' => gmdate('c'),
|
'reviewed_at' => gmdate('c'),
|
||||||
@@ -206,54 +249,6 @@ final class RegistrationService
|
|||||||
return ['success' => false, 'errors' => ['Die Registrierung konnte nicht aktualisiert werden.']];
|
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 [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'request' => $request,
|
'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
|
private function writeLdifDraft(string $id, string $username, string $content): string
|
||||||
{
|
{
|
||||||
$directory = $this->resolveProjectPath((string) ($this->config['ldif_export_dir'] ?? 'data/registration-ldif'));
|
$directory = $this->resolveProjectPath((string) ($this->config['ldif_export_dir'] ?? 'data/registration-ldif'));
|
||||||
|
|||||||
Reference in New Issue
Block a user