ldaü
This commit is contained in:
@@ -8,14 +8,24 @@ return [
|
||||
'ldif_export_dir' => 'data/registration-ldif',
|
||||
'approver_groups' => ['administrators', 'appadmin'],
|
||||
'approver_usernames' => [],
|
||||
'password_crypto_key' => getenv('REGISTRATION_PASSWORD_KEY') ?: '',
|
||||
'ldap' => [
|
||||
'enabled' => true,
|
||||
'host' => getenv('LDAP_HOST') ?: '127.0.0.1',
|
||||
'port' => (int) (getenv('LDAP_PORT') ?: 389),
|
||||
'bind_dn' => getenv('LDAP_BIND_DN') ?: '',
|
||||
'bind_password' => getenv('LDAP_BIND_PASSWORD') ?: '',
|
||||
'use_starttls' => filter_var(getenv('LDAP_USE_STARTTLS') ?: 'false', FILTER_VALIDATE_BOOL),
|
||||
'users_dn' => 'cn=users,dc=nas,dc=kusche,dc=berlin',
|
||||
'default_groups' => [
|
||||
'cn=generaluser,cn=groups,dc=nas,dc=kusche,dc=berlin',
|
||||
],
|
||||
'group_assignment_mode' => getenv('LDAP_GROUP_ASSIGNMENT_MODE') ?: 'group_memberuid',
|
||||
'group_member_attribute' => getenv('LDAP_GROUP_MEMBER_ATTRIBUTE') ?: 'memberUid',
|
||||
'default_login_shell' => '/bin/sh',
|
||||
'home_directory_prefix' => '/home',
|
||||
'shadow_expire' => 1,
|
||||
'active_shadow_expire' => -1,
|
||||
'shadow_flag' => 0,
|
||||
'shadow_inactive' => 0,
|
||||
'shadow_max' => 99999,
|
||||
|
||||
205
public/admin/ldap-test/index.php
Normal file
205
public/admin/ldap-test/index.php
Normal file
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
|
||||
|
||||
use App\ConfigLoader;
|
||||
use App\LdapProvisioner;
|
||||
use App\RegistrationAccess;
|
||||
|
||||
session_start();
|
||||
|
||||
$projectRoot = dirname(__DIR__, 3);
|
||||
$config = ConfigLoader::load($projectRoot, 'registration');
|
||||
$access = new RegistrationAccess($config);
|
||||
|
||||
if (!$access->canManage()) {
|
||||
http_response_code(403);
|
||||
echo 'Kein Zugriff auf LDAP-Test.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$ldap = new LdapProvisioner($config);
|
||||
$csrfToken = (string) ($_SESSION['ldap_test_token'] ?? '');
|
||||
|
||||
if ($csrfToken === '') {
|
||||
$csrfToken = bin2hex(random_bytes(16));
|
||||
$_SESSION['ldap_test_token'] = $csrfToken;
|
||||
}
|
||||
|
||||
$serviceResult = $ldap->testServiceBind();
|
||||
$messages = [];
|
||||
$errors = [];
|
||||
$loginForm = ['username' => '', 'password' => ''];
|
||||
$passwordForm = ['username' => '', 'current_password' => '', 'new_password' => '', 'new_password_confirm' => ''];
|
||||
|
||||
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 === 'test-login') {
|
||||
$loginForm['username'] = trim((string) ($_POST['username'] ?? ''));
|
||||
$loginForm['password'] = (string) ($_POST['password'] ?? '');
|
||||
|
||||
if ($loginForm['username'] === '' || $loginForm['password'] === '') {
|
||||
$errors[] = 'Für den Login-Test sind Benutzername und Passwort erforderlich.';
|
||||
} else {
|
||||
$result = $ldap->testUserCredentials($loginForm['username'], $loginForm['password']);
|
||||
|
||||
if ($result['success'] ?? false) {
|
||||
$messages[] = $result['message'] . ' DN: ' . (string) ($result['dn'] ?? '');
|
||||
} else {
|
||||
$errors[] = (string) ($result['message'] ?? 'LDAP-Login fehlgeschlagen.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'change-password') {
|
||||
$passwordForm['username'] = trim((string) ($_POST['username'] ?? ''));
|
||||
$passwordForm['current_password'] = (string) ($_POST['current_password'] ?? '');
|
||||
$passwordForm['new_password'] = (string) ($_POST['new_password'] ?? '');
|
||||
$passwordForm['new_password_confirm'] = (string) ($_POST['new_password_confirm'] ?? '');
|
||||
|
||||
if ($passwordForm['username'] === '') {
|
||||
$errors[] = 'Für die Passwortänderung ist ein Benutzername erforderlich.';
|
||||
}
|
||||
|
||||
if ($passwordForm['current_password'] === '') {
|
||||
$errors[] = 'Bitte das aktuelle Passwort angeben, damit der Login vorab geprüft werden kann.';
|
||||
}
|
||||
|
||||
if (strlen($passwordForm['new_password']) < 12) {
|
||||
$errors[] = 'Das neue Passwort muss mindestens 12 Zeichen lang sein.';
|
||||
}
|
||||
|
||||
if ($passwordForm['new_password'] !== $passwordForm['new_password_confirm']) {
|
||||
$errors[] = 'Die neuen Passwörter stimmen nicht überein.';
|
||||
}
|
||||
|
||||
if ($errors === []) {
|
||||
$loginCheck = $ldap->testUserCredentials($passwordForm['username'], $passwordForm['current_password']);
|
||||
|
||||
if (!(bool) ($loginCheck['success'] ?? false)) {
|
||||
$errors[] = 'Der Vorab-Login mit aktuellem Passwort ist fehlgeschlagen: ' . (string) ($loginCheck['message'] ?? '');
|
||||
} else {
|
||||
$result = $ldap->updateUserPassword($passwordForm['username'], $passwordForm['new_password']);
|
||||
|
||||
if ($result['success'] ?? false) {
|
||||
$messages[] = $result['message'] . ' DN: ' . (string) ($result['dn'] ?? '');
|
||||
$passwordForm = ['username' => '', 'current_password' => '', 'new_password' => '', 'new_password_confirm' => ''];
|
||||
} 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">Hier kannst du direkt den LDAP-Login prüfen und das Passwort inklusive Samba-Feldern aktualisieren.</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>
|
||||
|
||||
<?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">
|
||||
<section class="admin-detail">
|
||||
<h2>Login testen</h2>
|
||||
<form class="login-form" method="post" action="/admin/ldap-test">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES) ?>">
|
||||
<input type="hidden" name="action" value="test-login">
|
||||
|
||||
<label class="login-field">
|
||||
<span>Benutzername</span>
|
||||
<input type="text" name="username" value="<?= htmlspecialchars($loginForm['username'], ENT_QUOTES) ?>" required>
|
||||
</label>
|
||||
|
||||
<label class="login-field">
|
||||
<span>Passwort</span>
|
||||
<input type="password" name="password" value="" required>
|
||||
</label>
|
||||
|
||||
<div class="login-actions">
|
||||
<button class="login-button login-button-primary" type="submit">LDAP-Login prüfen</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="admin-detail">
|
||||
<h2>Passwort ändern</h2>
|
||||
<form class="login-form" method="post" action="/admin/ldap-test">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES) ?>">
|
||||
<input type="hidden" name="action" value="change-password">
|
||||
|
||||
<label class="login-field">
|
||||
<span>Benutzername</span>
|
||||
<input type="text" name="username" value="<?= htmlspecialchars($passwordForm['username'], ENT_QUOTES) ?>" required>
|
||||
</label>
|
||||
|
||||
<label class="login-field">
|
||||
<span>Aktuelles Passwort</span>
|
||||
<input type="password" name="current_password" value="" required>
|
||||
</label>
|
||||
|
||||
<label class="login-field">
|
||||
<span>Neues Passwort</span>
|
||||
<input type="password" name="new_password" value="" required>
|
||||
</label>
|
||||
|
||||
<label class="login-field">
|
||||
<span>Neues Passwort wiederholen</span>
|
||||
<input type="password" name="new_password_confirm" value="" required>
|
||||
</label>
|
||||
|
||||
<div class="login-actions">
|
||||
<button class="login-button login-button-primary" type="submit">Passwort im LDAP und SMB ändern</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -48,7 +48,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (($result['success'] ?? false) !== true) {
|
||||
$errors = array_values(array_map('strval', (array) ($result['errors'] ?? [])));
|
||||
} else {
|
||||
$notice = 'Registrierung freigegeben. Ein LDIF-Entwurf für den inaktiven Benutzer wurde erzeugt.';
|
||||
$notice = 'Registrierung freigegeben und als inaktiver LDAP-Benutzer angelegt.';
|
||||
}
|
||||
} elseif ($action === 'reject') {
|
||||
$result = $registration->reject($selectedId, (string) ($_POST['reject_reason'] ?? ''), $reviewer);
|
||||
@@ -79,7 +79,7 @@ $provisioning = is_array($selected['provisioning'] ?? null) ? $selected['provisi
|
||||
<div class="login-panel-top">
|
||||
<p class="login-kicker">Kusche.Berlin</p>
|
||||
<h1>Registrierungsfreigaben</h1>
|
||||
<p class="login-copy">Anfragen werden erst nach Prüfung für LDAP und NAS vorbereitet. Die Freigabe erzeugt aktuell einen LDIF-Entwurf für einen inaktiven Benutzer.</p>
|
||||
<p class="login-copy">Anfragen werden erst nach Prüfung direkt ins LDAP geschrieben. Dabei werden auch Unix- und Samba-Felder gesetzt, der Benutzer bleibt aber zunächst inaktiv.</p>
|
||||
</div>
|
||||
|
||||
<?php if ($notice !== null): ?>
|
||||
@@ -185,8 +185,8 @@ $provisioning = is_array($selected['provisioning'] ?? null) ? $selected['provisi
|
||||
</label>
|
||||
|
||||
<label class="login-field login-field-span-2">
|
||||
<span>memberOf als CSV</span>
|
||||
<input type="text" name="member_of_csv" value="<?= htmlspecialchars((string) ($provisioning['member_of_csv'] ?? 'cn=generaluser,cn=groups,dc=nas,dc=kusche,dc=berlin'), ENT_QUOTES) ?>">
|
||||
<span>Gruppen-DNs, eine Zeile pro Eintrag</span>
|
||||
<textarea name="member_of_list" rows="4"><?= htmlspecialchars((string) ($provisioning['member_of_list'] ?? "cn=generaluser,cn=groups,dc=nas,dc=kusche,dc=berlin"), ENT_QUOTES) ?></textarea>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -196,7 +196,7 @@ $provisioning = is_array($selected['provisioning'] ?? null) ? $selected['provisi
|
||||
</label>
|
||||
|
||||
<div class="login-actions">
|
||||
<button class="login-button login-button-primary" type="submit" name="action" value="approve">Freigeben und LDIF erzeugen</button>
|
||||
<button class="login-button login-button-primary" type="submit" name="action" value="approve">Freigeben und LDAP anlegen</button>
|
||||
<button class="login-button login-button-danger" type="submit" name="action" value="reject">Ablehnen</button>
|
||||
</div>
|
||||
|
||||
@@ -206,9 +206,26 @@ $provisioning = is_array($selected['provisioning'] ?? null) ? $selected['provisi
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<?php if (((string) ($provisioning['ldap_dn'] ?? '')) !== ''): ?>
|
||||
<div class="login-notice login-notice-success">
|
||||
<strong>LDAP-Anlage</strong>
|
||||
<p>DN: <?= htmlspecialchars((string) ($provisioning['ldap_dn'] ?? ''), ENT_QUOTES) ?></p>
|
||||
<?php if (is_array($provisioning['group_updates'] ?? null) && $provisioning['group_updates'] !== []): ?>
|
||||
<p>Gruppen: <?= htmlspecialchars(implode(', ', array_map('strval', $provisioning['group_updates'])), ENT_QUOTES) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (((string) ($provisioning['error'] ?? '')) !== ''): ?>
|
||||
<div class="login-notice login-notice-error">
|
||||
<strong>Provisionierungsfehler</strong>
|
||||
<p><?= htmlspecialchars((string) ($provisioning['error'] ?? ''), ENT_QUOTES) ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (((string) ($provisioning['ldif_path'] ?? '')) !== '' && is_file((string) $provisioning['ldif_path'])): ?>
|
||||
<div class="login-notice login-notice-info">
|
||||
<strong>LDIF-Entwurf</strong>
|
||||
<strong>LDIF-Fallback</strong>
|
||||
<p><?= htmlspecialchars((string) ($provisioning['ldif_path'] ?? ''), ENT_QUOTES) ?></p>
|
||||
<pre class="login-pre"><?= htmlspecialchars((string) file_get_contents((string) $provisioning['ldif_path']), ENT_QUOTES) ?></pre>
|
||||
</div>
|
||||
|
||||
@@ -34,6 +34,8 @@ $form = [
|
||||
'family_name' => '',
|
||||
'email' => '',
|
||||
'note' => '',
|
||||
'password' => '',
|
||||
'password_confirm' => '',
|
||||
];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
@@ -43,6 +45,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
'family_name' => trim((string) ($_POST['family_name'] ?? '')),
|
||||
'email' => trim((string) ($_POST['email'] ?? '')),
|
||||
'note' => trim((string) ($_POST['note'] ?? '')),
|
||||
'password' => (string) ($_POST['password'] ?? ''),
|
||||
'password_confirm' => (string) ($_POST['password_confirm'] ?? ''),
|
||||
];
|
||||
|
||||
$postedToken = (string) ($_POST['csrf_token'] ?? '');
|
||||
@@ -65,6 +69,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
'family_name' => '',
|
||||
'email' => '',
|
||||
'note' => '',
|
||||
'password' => '',
|
||||
'password_confirm' => '',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -137,9 +143,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
<textarea name="note" rows="5" placeholder="Optional: Bezug zur Familie, Einladung, gewünschte Nutzung, Rückfragekontakt"><?= htmlspecialchars($form['note'], ENT_QUOTES) ?></textarea>
|
||||
</label>
|
||||
|
||||
<div class="login-form-grid">
|
||||
<label class="login-field">
|
||||
<span>Passwort</span>
|
||||
<input type="password" name="password" value="" required>
|
||||
</label>
|
||||
|
||||
<label class="login-field">
|
||||
<span>Passwort wiederholen</span>
|
||||
<input type="password" name="password_confirm" value="" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="login-notice login-notice-info">
|
||||
<strong>Wichtig</strong>
|
||||
<p>Das Passwort wird erst nach Freigabe gesetzt. Neue Registrierungen erhalten zunächst keinen aktiven Desktop-Zugang.</p>
|
||||
<p>Das Passwort wird verschlüsselt zwischengespeichert und bei Freigabe direkt für LDAP und SMB gesetzt. Neue Registrierungen erhalten zunächst trotzdem keinen aktiven Desktop-Zugang.</p>
|
||||
</div>
|
||||
|
||||
<div class="login-actions">
|
||||
|
||||
404
src/App/LdapProvisioner.php
Normal file
404
src/App/LdapProvisioner.php
Normal file
@@ -0,0 +1,404 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class LdapProvisioner
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly array $config,
|
||||
) {
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return (bool) ($this->config['ldap']['enabled'] ?? false);
|
||||
}
|
||||
|
||||
public function canUseLdap(): bool
|
||||
{
|
||||
return function_exists('ldap_connect')
|
||||
&& function_exists('ldap_bind')
|
||||
&& function_exists('ldap_add');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{success: bool, message: string}
|
||||
*/
|
||||
public function testServiceBind(): array
|
||||
{
|
||||
try {
|
||||
$link = $this->connectAsService();
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Service-Bind erfolgreich.',
|
||||
];
|
||||
} catch (\Throwable $exception) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $exception->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{success: bool, message: string, dn?: string}
|
||||
*/
|
||||
public function testUserCredentials(string $username, string $password): array
|
||||
{
|
||||
try {
|
||||
if (!$this->canUseLdap()) {
|
||||
throw new RuntimeException('Die PHP-LDAP-Erweiterung ist auf diesem Server nicht verfügbar.');
|
||||
}
|
||||
|
||||
$link = $this->connect();
|
||||
$userDn = $this->findUserDnByUid($link, $username);
|
||||
|
||||
if ($userDn === null) {
|
||||
throw new RuntimeException('Benutzer wurde im LDAP nicht gefunden.');
|
||||
}
|
||||
|
||||
if (!@ldap_bind($link, $userDn, $password)) {
|
||||
throw new RuntimeException('LDAP-Login fehlgeschlagen: ' . $this->lastError($link));
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'LDAP-Login erfolgreich.',
|
||||
'dn' => $userDn,
|
||||
];
|
||||
} catch (\Throwable $exception) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $exception->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{success: bool, message: string, dn?: string}
|
||||
*/
|
||||
public function updateUserPassword(string $username, string $newPassword): array
|
||||
{
|
||||
try {
|
||||
$link = $this->connectAsService();
|
||||
$userDn = $this->findUserDnByUid($link, $username);
|
||||
|
||||
if ($userDn === null) {
|
||||
throw new RuntimeException('Benutzer wurde im LDAP nicht gefunden.');
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$shadowLastChange = (string) floor($now / 86400);
|
||||
$ntHash = strtoupper(hash('md4', iconv('UTF-8', 'UTF-16LE', $newPassword)));
|
||||
$unixHash = $this->cryptSha512($newPassword);
|
||||
$modifications = [
|
||||
'userPassword' => $unixHash,
|
||||
'sambaNTPassword' => $ntHash,
|
||||
'sambaLMPassword' => 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
|
||||
'sambaPwdLastSet' => (string) $now,
|
||||
'shadowLastChange' => $shadowLastChange,
|
||||
];
|
||||
|
||||
if (!$this->isInactiveShadowExpiry()) {
|
||||
$modifications['shadowExpire'] = (string) ($this->config['ldap']['active_shadow_expire'] ?? -1);
|
||||
}
|
||||
|
||||
if (!@ldap_modify($link, $userDn, $modifications)) {
|
||||
throw new RuntimeException('Passwortänderung im LDAP fehlgeschlagen: ' . $this->lastError($link));
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Passwort wurde im LDAP und für Samba aktualisiert.',
|
||||
'dn' => $userDn,
|
||||
];
|
||||
} catch (\Throwable $exception) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $exception->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $request
|
||||
* @param array<string, mixed> $provisioning
|
||||
* @return array{dn: string, group_updates: array<int, string>}
|
||||
*/
|
||||
public function createInactiveUser(array $request, array $provisioning, string $plainPassword): array
|
||||
{
|
||||
if (!$this->isEnabled()) {
|
||||
throw new RuntimeException('LDAP-Provisionierung ist deaktiviert.');
|
||||
}
|
||||
|
||||
if (!$this->canUseLdap()) {
|
||||
throw new RuntimeException('Die PHP-LDAP-Erweiterung ist auf diesem Server nicht verfügbar.');
|
||||
}
|
||||
|
||||
$userDn = 'uid=' . (string) ($request['username'] ?? '') . ',' . (string) ($this->config['ldap']['users_dn'] ?? '');
|
||||
$link = $this->connectAsService();
|
||||
|
||||
$entry = $this->buildEntry($request, $provisioning, $plainPassword);
|
||||
|
||||
if (!@ldap_add($link, $userDn, $entry)) {
|
||||
throw new RuntimeException('LDAP-Benutzer konnte nicht angelegt werden: ' . $this->lastError($link));
|
||||
}
|
||||
|
||||
$groupUpdates = $this->assignGroups($link, (string) ($request['username'] ?? ''), (string) $userDn, $provisioning);
|
||||
|
||||
return [
|
||||
'dn' => $userDn,
|
||||
'group_updates' => $groupUpdates,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $request
|
||||
* @param array<string, mixed> $provisioning
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildEntry(array $request, array $provisioning, string $plainPassword): array
|
||||
{
|
||||
$username = (string) ($request['username'] ?? '');
|
||||
$givenName = trim((string) ($request['given_name'] ?? ''));
|
||||
$familyName = trim((string) ($request['family_name'] ?? ''));
|
||||
$displayName = trim((string) ($provisioning['display_name'] ?? $request['display_name'] ?? ''));
|
||||
$cn = trim((string) ($provisioning['cn'] ?? ($displayName !== '' ? $displayName : $username)));
|
||||
$gecos = trim((string) ($provisioning['gecos'] ?? ($displayName !== '' ? $displayName : trim($givenName . ' ' . $familyName))));
|
||||
$uidNumber = trim((string) ($provisioning['uid_number'] ?? ''));
|
||||
$gidNumber = trim((string) ($provisioning['gid_number'] ?? ''));
|
||||
$homeDirectory = trim((string) ($provisioning['home_directory'] ?? ''));
|
||||
$loginShell = trim((string) ($provisioning['login_shell'] ?? ($this->config['ldap']['default_login_shell'] ?? '/bin/sh')));
|
||||
$mail = trim((string) ($request['email'] ?? ''));
|
||||
$appleGeneratedUid = strtoupper(trim((string) ($provisioning['apple_generateduid'] ?? $this->generateUuidV4())));
|
||||
$sambaSid = trim((string) ($provisioning['samba_sid'] ?? ''));
|
||||
$ntHash = strtoupper(hash('md4', iconv('UTF-8', 'UTF-16LE', $plainPassword)));
|
||||
$unixHash = $this->cryptSha512($plainPassword);
|
||||
$now = time();
|
||||
$shadowLastChange = (string) floor($now / 86400);
|
||||
|
||||
return [
|
||||
'objectClass' => [
|
||||
'apple-user',
|
||||
'extensibleObject',
|
||||
'inetOrgPerson',
|
||||
'organizationalPerson',
|
||||
'person',
|
||||
'posixAccount',
|
||||
'sambaIdmapEntry',
|
||||
'sambaSamAccount',
|
||||
'shadowAccount',
|
||||
'top',
|
||||
],
|
||||
'cn' => $cn,
|
||||
'gidNumber' => $gidNumber,
|
||||
'homeDirectory' => $homeDirectory,
|
||||
'sambaSID' => $sambaSid,
|
||||
'sn' => $familyName !== '' ? $familyName : $username,
|
||||
'uid' => $username,
|
||||
'uidNumber' => $uidNumber,
|
||||
'apple-generateduid' => $appleGeneratedUid,
|
||||
'authAuthority' => ';basic;',
|
||||
'displayName' => $displayName !== '' ? $displayName : $username,
|
||||
'gecos' => $gecos,
|
||||
'givenName' => $givenName,
|
||||
'loginShell' => $loginShell,
|
||||
'mail' => $mail,
|
||||
'sambaAcctFlags' => '[U ]',
|
||||
'sambaLMPassword' => 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
|
||||
'sambaNTPassword' => $ntHash,
|
||||
'sambaPasswordHistory' => '0000000000000000000000000000000000000000000000000000000000000000',
|
||||
'sambaPwdLastSet' => (string) $now,
|
||||
'shadowExpire' => (string) ($this->config['ldap']['shadow_expire'] ?? 1),
|
||||
'shadowFlag' => (string) ($this->config['ldap']['shadow_flag'] ?? 0),
|
||||
'shadowInactive' => (string) ($this->config['ldap']['shadow_inactive'] ?? 0),
|
||||
'shadowLastChange' => $shadowLastChange,
|
||||
'shadowMax' => (string) ($this->config['ldap']['shadow_max'] ?? 99999),
|
||||
'shadowMin' => (string) ($this->config['ldap']['shadow_min'] ?? 0),
|
||||
'shadowWarning' => (string) ($this->config['ldap']['shadow_warning'] ?? 7),
|
||||
'userPassword' => $unixHash,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource|\LDAP\Connection $link
|
||||
* @param array<string, mixed> $provisioning
|
||||
* @return array<int, string>
|
||||
*/
|
||||
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'] ?? [])));
|
||||
$mode = (string) ($this->config['ldap']['group_assignment_mode'] ?? 'group_memberuid');
|
||||
$updated = [];
|
||||
|
||||
if ($groupDns === [] || $mode === 'none') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($mode === 'user_memberof') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$attribute = (string) ($this->config['ldap']['group_member_attribute'] ?? 'memberUid');
|
||||
$value = $attribute === 'member' ? $userDn : $username;
|
||||
|
||||
foreach ($groupDns as $groupDn) {
|
||||
$result = @ldap_mod_add($link, $groupDn, [$attribute => [$value]]);
|
||||
|
||||
if ($result === false) {
|
||||
$errorCode = @ldap_errno($link);
|
||||
|
||||
if ($errorCode === 20 || $errorCode === 68) {
|
||||
$updated[] = $groupDn . ' (bereits vorhanden)';
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new RuntimeException('LDAP-Gruppenaktualisierung fehlgeschlagen für ' . $groupDn . ': ' . $this->lastError($link));
|
||||
}
|
||||
|
||||
$updated[] = $groupDn;
|
||||
}
|
||||
|
||||
return $updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return resource|\LDAP\Connection
|
||||
*/
|
||||
private function connect()
|
||||
{
|
||||
$host = trim((string) ($this->config['ldap']['host'] ?? ''));
|
||||
$port = (int) ($this->config['ldap']['port'] ?? 389);
|
||||
|
||||
if ($host === '') {
|
||||
throw new RuntimeException('LDAP_HOST ist nicht gesetzt.');
|
||||
}
|
||||
|
||||
$link = ldap_connect($host, $port);
|
||||
|
||||
if ($link === false) {
|
||||
throw new RuntimeException('LDAP-Verbindung konnte nicht aufgebaut werden.');
|
||||
}
|
||||
|
||||
ldap_set_option($link, LDAP_OPT_PROTOCOL_VERSION, 3);
|
||||
ldap_set_option($link, LDAP_OPT_REFERRALS, 0);
|
||||
|
||||
if ((bool) ($this->config['ldap']['use_starttls'] ?? false) && !@ldap_start_tls($link)) {
|
||||
throw new RuntimeException('LDAP StartTLS konnte nicht aktiviert werden: ' . $this->lastError($link));
|
||||
}
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return resource|\LDAP\Connection
|
||||
*/
|
||||
private function connectAsService()
|
||||
{
|
||||
$bindDn = trim((string) ($this->config['ldap']['bind_dn'] ?? ''));
|
||||
$bindPassword = (string) ($this->config['ldap']['bind_password'] ?? '');
|
||||
|
||||
if ($bindDn === '' || $bindPassword === '') {
|
||||
throw new RuntimeException('LDAP-Verbindungsdaten sind unvollständig.');
|
||||
}
|
||||
|
||||
$link = $this->connect();
|
||||
|
||||
if (!@ldap_bind($link, $bindDn, $bindPassword)) {
|
||||
throw new RuntimeException('LDAP-Bind fehlgeschlagen: ' . $this->lastError($link));
|
||||
}
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource|\LDAP\Connection $link
|
||||
*/
|
||||
private function findUserDnByUid($link, string $username): ?string
|
||||
{
|
||||
$baseDn = (string) ($this->config['ldap']['users_dn'] ?? '');
|
||||
|
||||
if ($baseDn === '') {
|
||||
throw new RuntimeException('LDAP users_dn ist nicht gesetzt.');
|
||||
}
|
||||
|
||||
$filter = sprintf('(uid=%s)', $this->ldapEscape($username, '', LDAP_ESCAPE_FILTER));
|
||||
$search = @ldap_search($link, $baseDn, $filter, ['dn']);
|
||||
|
||||
if ($search === false) {
|
||||
throw new RuntimeException('LDAP-Suche fehlgeschlagen: ' . $this->lastError($link));
|
||||
}
|
||||
|
||||
$entries = ldap_get_entries($link, $search);
|
||||
|
||||
if (!is_array($entries) || (int) ($entries['count'] ?? 0) < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (string) ($entries[0]['dn'] ?? '');
|
||||
}
|
||||
|
||||
private function isInactiveShadowExpiry(): bool
|
||||
{
|
||||
return (string) ($this->config['ldap']['shadow_expire'] ?? 1) !== (string) ($this->config['ldap']['active_shadow_expire'] ?? -1);
|
||||
}
|
||||
|
||||
private function ldapEscape(string $value, string $ignore = '', int $flags = 0): string
|
||||
{
|
||||
if (function_exists('ldap_escape')) {
|
||||
return ldap_escape($value, $ignore, $flags);
|
||||
}
|
||||
|
||||
$search = ['\\', '*', '(', ')', "\x00"];
|
||||
$replace = ['\\5c', '\\2a', '\\28', '\\29', '\\00'];
|
||||
|
||||
return str_replace($search, $replace, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function parseGroupDns(string $value): array
|
||||
{
|
||||
$lines = preg_split('/\r\n|\r|\n/', $value) ?: [];
|
||||
|
||||
return array_values(array_filter(array_map('trim', $lines)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource|\LDAP\Connection $link
|
||||
*/
|
||||
private function lastError($link): string
|
||||
{
|
||||
$error = @ldap_error($link);
|
||||
return is_string($error) && $error !== '' ? $error : 'Unbekannter LDAP-Fehler';
|
||||
}
|
||||
|
||||
private function generateUuidV4(): string
|
||||
{
|
||||
$bytes = random_bytes(16);
|
||||
$bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
|
||||
$bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
|
||||
|
||||
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));
|
||||
}
|
||||
|
||||
private function cryptSha512(string $password): string
|
||||
{
|
||||
$salt = '$6$' . bin2hex(random_bytes(8)) . '$';
|
||||
$hash = crypt($password, $salt);
|
||||
|
||||
return str_starts_with($hash, '$6$') ? '{CRYPT}' . $hash : $hash;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ final class RegistrationLdifBuilder
|
||||
$gidNumber = trim((string) ($provisioning['gid_number'] ?? ''));
|
||||
$homeDirectory = trim((string) ($provisioning['home_directory'] ?? ''));
|
||||
$loginShell = trim((string) ($provisioning['login_shell'] ?? ($this->config['ldap']['default_login_shell'] ?? '/bin/sh')));
|
||||
$memberOf = array_values(array_filter(array_map('trim', explode(',', (string) ($provisioning['member_of_csv'] ?? '')))));
|
||||
$memberOf = $this->parseGroupDns((string) ($provisioning['member_of_list'] ?? ''));
|
||||
$displayName = trim((string) ($provisioning['display_name'] ?? $request['display_name'] ?? ''));
|
||||
$givenName = trim((string) ($request['given_name'] ?? ''));
|
||||
$familyName = trim((string) ($request['family_name'] ?? ''));
|
||||
@@ -137,6 +137,16 @@ final class RegistrationLdifBuilder
|
||||
return implode(PHP_EOL, $lines) . PHP_EOL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function parseGroupDns(string $value): array
|
||||
{
|
||||
$lines = preg_split('/\r\n|\r|\n/', $value) ?: [];
|
||||
|
||||
return array_values(array_filter(array_map('trim', $lines)));
|
||||
}
|
||||
|
||||
private function generateUuidV4(): string
|
||||
{
|
||||
$bytes = random_bytes(16);
|
||||
|
||||
73
src/App/RegistrationSecretBox.php
Normal file
73
src/App/RegistrationSecretBox.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class RegistrationSecretBox
|
||||
{
|
||||
private const CIPHER = 'aes-256-gcm';
|
||||
|
||||
public function __construct(
|
||||
private readonly string $keyMaterial,
|
||||
) {
|
||||
}
|
||||
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return trim($this->keyMaterial) !== '';
|
||||
}
|
||||
|
||||
public function encrypt(string $plaintext): string
|
||||
{
|
||||
$key = $this->normalizedKey();
|
||||
$iv = random_bytes(12);
|
||||
$tag = '';
|
||||
$ciphertext = openssl_encrypt($plaintext, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv, $tag);
|
||||
|
||||
if ($ciphertext === false) {
|
||||
throw new RuntimeException('Das Passwort konnte nicht verschlüsselt werden.');
|
||||
}
|
||||
|
||||
return base64_encode($iv . $tag . $ciphertext);
|
||||
}
|
||||
|
||||
public function decrypt(string $encoded): string
|
||||
{
|
||||
$raw = base64_decode($encoded, true);
|
||||
|
||||
if ($raw === false || strlen($raw) < 28) {
|
||||
throw new RuntimeException('Das gespeicherte Passwort ist beschädigt.');
|
||||
}
|
||||
|
||||
$key = $this->normalizedKey();
|
||||
$iv = substr($raw, 0, 12);
|
||||
$tag = substr($raw, 12, 16);
|
||||
$ciphertext = substr($raw, 28);
|
||||
$plaintext = openssl_decrypt($ciphertext, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv, $tag);
|
||||
|
||||
if ($plaintext === false) {
|
||||
throw new RuntimeException('Das gespeicherte Passwort konnte nicht entschlüsselt werden.');
|
||||
}
|
||||
|
||||
return $plaintext;
|
||||
}
|
||||
|
||||
private function normalizedKey(): string
|
||||
{
|
||||
if (!$this->isConfigured()) {
|
||||
throw new RuntimeException('REGISTRATION_PASSWORD_KEY ist nicht gesetzt.');
|
||||
}
|
||||
|
||||
$trimmed = trim($this->keyMaterial);
|
||||
$decoded = base64_decode($trimmed, true);
|
||||
|
||||
if ($decoded !== false && strlen($decoded) >= 32) {
|
||||
return substr($decoded, 0, 32);
|
||||
}
|
||||
|
||||
return hash('sha256', $trimmed, true);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ final class RegistrationService
|
||||
{
|
||||
private RegistrationStore $store;
|
||||
private RegistrationLdifBuilder $ldifBuilder;
|
||||
private RegistrationSecretBox $secretBox;
|
||||
private LdapProvisioner $ldapProvisioner;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
@@ -19,6 +21,8 @@ final class RegistrationService
|
||||
$storagePath = $this->resolveProjectPath((string) ($this->config['storage_path'] ?? 'data/registration-requests.json'));
|
||||
$this->store = new RegistrationStore($storagePath);
|
||||
$this->ldifBuilder = new RegistrationLdifBuilder($this->config);
|
||||
$this->secretBox = new RegistrationSecretBox((string) ($this->config['password_crypto_key'] ?? ''));
|
||||
$this->ldapProvisioner = new LdapProvisioner($this->config);
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
@@ -53,6 +57,8 @@ final class RegistrationService
|
||||
$familyName = trim((string) ($input['family_name'] ?? ''));
|
||||
$email = strtolower(trim((string) ($input['email'] ?? '')));
|
||||
$note = trim((string) ($input['note'] ?? ''));
|
||||
$password = (string) ($input['password'] ?? '');
|
||||
$passwordConfirm = (string) ($input['password_confirm'] ?? '');
|
||||
$displayName = trim($givenName . ' ' . $familyName);
|
||||
$errors = [];
|
||||
|
||||
@@ -72,6 +78,18 @@ final class RegistrationService
|
||||
$errors[] = 'Bitte gib eine gültige E-Mail-Adresse an.';
|
||||
}
|
||||
|
||||
if (strlen($password) < 12) {
|
||||
$errors[] = 'Das Passwort muss mindestens 12 Zeichen lang sein.';
|
||||
}
|
||||
|
||||
if ($password !== $passwordConfirm) {
|
||||
$errors[] = 'Die Passwörter stimmen nicht überein.';
|
||||
}
|
||||
|
||||
if (!$this->secretBox->isConfigured()) {
|
||||
$errors[] = 'Die Registrierungsverschlüsselung ist noch nicht konfiguriert.';
|
||||
}
|
||||
|
||||
if ($note !== '' && mb_strlen($note) > 2000) {
|
||||
$errors[] = 'Die Zusatzinfo ist zu lang.';
|
||||
}
|
||||
@@ -100,6 +118,7 @@ final class RegistrationService
|
||||
'display_name' => $displayName,
|
||||
'email' => $email,
|
||||
'note' => $note,
|
||||
'password_secret' => $this->secretBox->encrypt($password),
|
||||
'origin' => [
|
||||
'ip' => (string) ($_SERVER['REMOTE_ADDR'] ?? ''),
|
||||
'user_agent' => (string) ($_SERVER['HTTP_USER_AGENT'] ?? ''),
|
||||
@@ -131,7 +150,7 @@ final class RegistrationService
|
||||
$gidNumber = trim((string) ($input['gid_number'] ?? ''));
|
||||
$homeDirectory = trim((string) ($input['home_directory'] ?? ''));
|
||||
$sambaSid = trim((string) ($input['samba_sid'] ?? ''));
|
||||
$memberOfCsv = trim((string) ($input['member_of_csv'] ?? ''));
|
||||
$memberOfList = trim((string) ($input['member_of_list'] ?? ''));
|
||||
$adminNote = trim((string) ($input['admin_note'] ?? ''));
|
||||
$errors = [];
|
||||
|
||||
@@ -156,7 +175,7 @@ final class RegistrationService
|
||||
'gid_number' => $gidNumber,
|
||||
'home_directory' => $homeDirectory,
|
||||
'login_shell' => trim((string) ($input['login_shell'] ?? ($this->config['ldap']['default_login_shell'] ?? '/bin/sh'))),
|
||||
'member_of_csv' => $memberOfCsv,
|
||||
'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'] ?? ''))),
|
||||
@@ -190,16 +209,50 @@ final class RegistrationService
|
||||
$ldif = $this->ldifBuilder->build($request, $provisioning);
|
||||
$ldifPath = $this->writeLdifDraft((string) ($request['id'] ?? ''), (string) ($request['username'] ?? ''), $ldif);
|
||||
|
||||
$request = $this->store->update(
|
||||
$id,
|
||||
static function (array $item) use ($ldifPath): array {
|
||||
$item['updated_at'] = gmdate('c');
|
||||
$item['provisioning']['ldif_path'] = $ldifPath;
|
||||
$item['provisioning']['status'] = 'ldif_generated';
|
||||
try {
|
||||
$passwordSecret = (string) ($request['password_secret'] ?? '');
|
||||
|
||||
return $item;
|
||||
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,
|
||||
|
||||
46
temp/LDAP_DIREKT_SETUP.md
Normal file
46
temp/LDAP_DIREKT_SETUP.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# LDAP Direkt-Setup
|
||||
|
||||
Damit die Freigabe direkt ins LDAP schreiben kann, muss auf dem Zielsystem Folgendes vorhanden sein:
|
||||
|
||||
## PHP-Erweiterung
|
||||
|
||||
- `php-ldap`
|
||||
|
||||
Lokal in meiner aktuellen Umgebung ist diese Erweiterung **nicht** geladen. Deshalb ist der Verbindungsweg hier nicht live getestet, nur die Syntax.
|
||||
|
||||
## Erforderliche Umgebungsvariablen
|
||||
|
||||
- `REGISTRATION_PASSWORD_KEY`
|
||||
- `LDAP_HOST`
|
||||
- `LDAP_PORT`
|
||||
- `LDAP_BIND_DN`
|
||||
- `LDAP_BIND_PASSWORD`
|
||||
- `LDAP_USE_STARTTLS`
|
||||
- optional `LDAP_GROUP_ASSIGNMENT_MODE`
|
||||
- optional `LDAP_GROUP_MEMBER_ATTRIBUTE`
|
||||
|
||||
## Beispiel
|
||||
|
||||
```env
|
||||
REGISTRATION_PASSWORD_KEY=bitte-einen-langen-zufallswert-oder-base64-key-verwenden
|
||||
LDAP_HOST=ldap.example.internal
|
||||
LDAP_PORT=389
|
||||
LDAP_BIND_DN=uid=serviceaccount,cn=users,dc=nas,dc=kusche,dc=berlin
|
||||
LDAP_BIND_PASSWORD=supersecret
|
||||
LDAP_USE_STARTTLS=true
|
||||
LDAP_GROUP_ASSIGNMENT_MODE=group_memberuid
|
||||
LDAP_GROUP_MEMBER_ATTRIBUTE=memberUid
|
||||
```
|
||||
|
||||
## Aktuelles Verhalten
|
||||
|
||||
- Registrierung speichert das gewünschte Passwort verschlüsselt zwischen
|
||||
- Admin-Freigabe schreibt den Benutzer direkt ins LDAP
|
||||
- dabei werden `userPassword` und `sambaNTPassword` gesetzt
|
||||
- der Benutzer wird zunächst mit inaktivem `shadowExpire` angelegt
|
||||
- zusätzlich wird weiter ein LDIF-Fallback erzeugt
|
||||
|
||||
## Wichtige offene Punkte
|
||||
|
||||
- Ob `shadowExpire=1` für Keycloak-Logins wirklich blockierend wirkt, hängt vom LDAP-/NAS-Verhalten ab.
|
||||
- Falls Keycloak trotzdem Login zulässt, brauchen wir als Nächstes noch einen expliziten Aktivierungsstatus oder eine Gruppen-/Attributprüfung im Loginflow.
|
||||
Reference in New Issue
Block a user