diff --git a/README.md b/README.md
index 638cc10..f961cf7 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,8 @@ Der komplette Ablauf ist im [Gitea-Workflow](https://git.eckertplayground.de/taa
| Powershell Script für einzelne Benutzer und CSV Import | Alle Fisis |
| UI/UX anpassen | Yasin B (@Muchentuchen), Alexander M (@Alexander), Torsten J (@tojacobs) |
+**Hinweis:** Die Passwortanforderungen (Mindestlänge, Kategorien, keine Teile des Benutzernamens) werden beim Erstellen validiert. Die Validierung ist in `scripts/powershell/create_users_csv.ps1` implementiert und die Mockup-UI (`docs/Mockup/index.html`) zeigt die Anforderungen und prüft sie clientseitig.
+
---
## Dokumentation
diff --git a/docs/Mockup/index.html b/docs/Mockup/index.html
index be042cd..e097688 100644
--- a/docs/Mockup/index.html
+++ b/docs/Mockup/index.html
@@ -262,6 +262,10 @@
background-color: var(--color-secondary);
}
+
+ /* Password hint and invalid input styles */
+ .password-hint { display:block; margin-top: 6px; font-size: 0.9rem; color: #6c757d; }
+ .invalid { border: 1px solid #c0152f; background-color: rgba(192,21,47,0.04); }
@@ -280,6 +284,7 @@
+ Das Passwort muss mindestens 7 Zeichen lang sein, darf keine größeren Teile des Benutzernamens enthalten und muss Zeichen aus mindestens 3 von 4 Kategorien enthalten: Großbuchstaben, Kleinbuchstaben, Ziffern, Sonderzeichen.
@@ -377,14 +382,60 @@
}
}
+ function validatePasswordJS(password, sam) {
+ const errors = [];
+ if (!password || password.length < 7) {
+ errors.push('Passwort muss mindestens 7 Zeichen lang sein.');
+ }
+ let categories = 0;
+ if (/[A-Z]/.test(password)) categories++;
+ if (/[a-z]/.test(password)) categories++;
+ if (/\d/.test(password)) categories++;
+ if (/[^A-Za-z0-9]/.test(password)) categories++;
+ if (categories < 3) {
+ errors.push('Passwort muss Zeichen aus mindestens 3 von 4 Kategorien enthalten.');
+ }
+ if (sam) {
+ const pwLower = password.toLowerCase();
+ const samLower = sam.toLowerCase();
+ if (pwLower.includes(samLower)) {
+ errors.push('Passwort darf den Benutzernamen nicht enthalten.');
+ } else {
+ const minLen = 4;
+ if (samLower.length >= minLen) {
+ outer: for (let len = minLen; len <= samLower.length; len++) {
+ for (let s = 0; s <= samLower.length - len; s++) {
+ const sub = samLower.substr(s, len);
+ if (pwLower.includes(sub)) {
+ errors.push('Passwort darf keine größeren Teile des Benutzernamens enthalten.');
+ break outer;
+ }
+ }
+ }
+ }
+ }
+ }
+ return errors;
+ }
+
document.getElementById('adForm').addEventListener('submit', (e) => {
e.preventDefault();
-
+
+ const firstname = document.getElementById('firstname').value.trim();
+ const lastname = document.getElementById('lastname').value.trim();
+ const samGuess = (firstname + lastname).replace(/\s+/g, '');
+ const password = document.getElementById('password').value;
+ const pwErrors = validatePasswordJS(password, samGuess);
+ if (pwErrors.length > 0) {
+ alert('Passwort ungültig:\n' + pwErrors.join('\n'));
+ return;
+ }
+
const formData = {
- firstname: document.getElementById('firstname').value,
- lastname: document.getElementById('lastname').value,
+ firstname: firstname,
+ lastname: lastname,
group: document.getElementById('group').value,
- password: document.getElementById('password').value,
+ password: password,
customFields: {}
};
@@ -442,6 +493,9 @@
// Datenzeilen erstellen (mit editierbaren Inputs)
tableBody.innerHTML = '';
+ const pwdHeaderIndex = headers.findIndex(h => /pass(word)?/i.test(h));
+ const samHeaderIndex = headers.findIndex(h => /(sam(accountname)?)|samaccountname/i.test(h));
+ let foundInvalid = false;
for (let i = 1; i < csvData.length; i++) {
const row = csvData[i];
const tr = document.createElement('tr');
@@ -454,6 +508,10 @@
input.dataset.col = index;
input.addEventListener('input', (e) => {
csvData[i][index] = e.target.value;
+ // live re-validate if this is password or sam column
+ if (index === pwdHeaderIndex || index === samHeaderIndex) {
+ validateCsvPasswords(headers);
+ }
});
td.appendChild(input);
tr.appendChild(td);
@@ -461,10 +519,53 @@
tableBody.appendChild(tr);
}
+ // Validate password column if present
+ function validateCsvPasswords(headersLocal) {
+ const pwdIdx = pwdHeaderIndex;
+ const samIdx = samHeaderIndex;
+ foundInvalid = false;
+ // Clear previous highlights/messages
+ const rows = tableBody.querySelectorAll('tr');
+ rows.forEach((r, idx) => {
+ r.querySelectorAll('input').forEach(inp => inp.classList.remove('invalid'));
+ });
+
+ if (pwdIdx >= 0) {
+ for (let r = 0; r < rows.length; r++) {
+ const inputs = rows[r].querySelectorAll('input');
+ const pwdVal = inputs[pwdIdx] ? inputs[pwdIdx].value : '';
+ const samVal = (samIdx >= 0 && inputs[samIdx]) ? inputs[samIdx].value : '';
+ const errs = validatePasswordJS(pwdVal, samVal);
+ if (errs.length > 0) {
+ foundInvalid = true;
+ if (inputs[pwdIdx]) inputs[pwdIdx].classList.add('invalid');
+ }
+ }
+ }
+
+ const previewInfo = previewDiv.querySelector('.preview-info');
+ if (!previewInfo) return;
+ if (foundInvalid) {
+ previewInfo.innerHTML = 'Hinweis: Einige Passwörter entsprechen nicht den Anforderungen. Bitte korrigieren Sie diese in der Tabelle bevor Sie importieren.';
+ } else {
+ previewInfo.innerHTML = 'Hinweis: Sie können die Werte direkt in der Tabelle bearbeiten, bevor Sie importieren.';
+ }
+ }
+
+ // initial validation run
+ validateCsvPasswords(headers);
+
previewDiv.style.display = 'block';
}
function importCSVData() {
+ // Prevent import if any password entries are invalid (highlighted)
+ const invalids = document.querySelectorAll('#csvTableBody input.invalid');
+ if (invalids.length > 0) {
+ alert('Import abgebrochen: Es gibt ungültige Passwörter in der CSV-Vorschau. Bitte korrigieren Sie diese zuerst.');
+ return;
+ }
+
console.log('Importierte CSV-Daten:', csvData);
alert(`${csvData.length - 1} Benutzer erfolgreich importiert!\n\nDaten in der Konsole (F12) einsehen.`);
cancelCSVPreview();
diff --git a/public/api/create_user.php b/public/api/create_user.php
index 9780627..5a57722 100644
--- a/public/api/create_user.php
+++ b/public/api/create_user.php
@@ -33,6 +33,50 @@ if ($sam === '' || $pass === '') {
exit;
}
+// Server-side password validation (same rules as CSV script)
+$passwordHint = "Das Passwort muss mindestens 7 Zeichen lang sein, darf keine größeren Teile des Benutzernamens enthalten und muss Zeichen aus mindestens 3 von 4 Kategorien enthalten: Großbuchstaben, Kleinbuchstaben, Ziffern, Sonderzeichen.";
+function validate_password_php(string $password, string $sam): array {
+ $errors = [];
+ if ($password === '' || mb_strlen($password) < 7) {
+ $errors[] = 'Passwort muss mindestens 7 Zeichen lang sein.';
+ }
+ $categories = 0;
+ if (preg_match('/[A-Z]/u', $password)) $categories++;
+ if (preg_match('/[a-z]/u', $password)) $categories++;
+ if (preg_match('/\d/', $password)) $categories++;
+ if (preg_match('/[^A-Za-z0-9]/u', $password)) $categories++;
+ if ($categories < 3) {
+ $errors[] = 'Passwort muss Zeichen aus mindestens 3 von 4 Kategorien enthalten.';
+ }
+ $samLower = mb_strtolower($sam);
+ $pwLower = mb_strtolower($password);
+ if ($samLower !== '' && mb_strpos($pwLower, $samLower) !== false) {
+ $errors[] = 'Passwort darf den Benutzernamen nicht enthalten.';
+ } else {
+ $minLen = 4;
+ if (mb_strlen($samLower) >= $minLen) {
+ $samLen = mb_strlen($samLower);
+ for ($len = $minLen; $len <= $samLen; $len++) {
+ for ($start = 0; $start <= $samLen - $len; $start++) {
+ $sub = mb_substr($samLower, $start, $len);
+ if (mb_strpos($pwLower, $sub) !== false) {
+ $errors[] = 'Passwort darf keine größeren Teile des Benutzernamens enthalten.';
+ break 2;
+ }
+ }
+ }
+ }
+ }
+ return $errors;
+}
+
+$pwErrors = validate_password_php($pass, $sam);
+if (count($pwErrors) > 0) {
+ $_SESSION['flash_error'] = 'Ungültiges Passwort: ' . implode(' | ', $pwErrors) . "\n\nHinweis: $passwordHint";
+ header('Location: ../index.php?route=createuser');
+ exit;
+}
+
if ($ou === '') {
$defaultOu = (string)($config['powershell']['default_ou'] ?? '');
if ($defaultOu !== '') {
diff --git a/public/views/createuser.php b/public/views/createuser.php
index 7e5f970..17ff793 100644
--- a/public/views/createuser.php
+++ b/public/views/createuser.php
@@ -72,6 +72,7 @@ declare(strict_types=1);
+ Das Passwort muss mindestens 7 Zeichen lang sein, darf keine größeren Teile des Benutzernamens enthalten und muss Zeichen aus mindestens 3 von 4 Kategorien enthalten: Großbuchstaben, Kleinbuchstaben, Ziffern, Sonderzeichen.
@@ -81,6 +82,51 @@ declare(strict_types=1);
+
diff --git a/scripts/powershell/create_user.ps1 b/scripts/powershell/create_user.ps1
index b49ec8a..011468b 100644
--- a/scripts/powershell/create_user.ps1
+++ b/scripts/powershell/create_user.ps1
@@ -32,7 +32,64 @@ $pass = [string]$payload.password
$ou = [string]($payload.ou)
$groups = [string]($payload.groups)
$dryRun = [bool]($payload.dry_run -as [bool])
+# Password hint and validation helper (German)
+$passwordHint = @"
+Das sollten die Anforderungen an das Passwort sein:
+- mindestens 7 Zeichen
+- darf den Benutzer-/Accountnamen nicht enthalten (bzw. keine zu großen Teile davon)
+- muss Zeichen aus mindestens 3 von 4 Kategorien enthalten:
+ 1) Großbuchstaben (A–Z)
+ 2) Kleinbuchstaben (a–z)
+ 3) Ziffern (0–9)
+ 4) Sonderzeichen (alles, was kein Buchstabe/Zahl ist, z. B. ! ? # _ - . , usw.)
+"@
+
+function Test-PasswordRequirements {
+ param(
+ [string]$Password,
+ [string]$SamAccountName
+ )
+
+ $errors = @()
+
+ if ([string]::IsNullOrEmpty($Password) -or $Password.Length -lt 7) {
+ $errors += 'Passwort muss mindestens 7 Zeichen lang sein.'
+ }
+
+ $categories = 0
+ if ($Password -match '[A-Z]') { $categories++ }
+ if ($Password -match '[a-z]') { $categories++ }
+ if ($Password -match '\d') { $categories++ }
+ if ($Password -match '[^A-Za-z0-9]') { $categories++ }
+ if ($categories -lt 3) {
+ $errors += 'Passwort muss Zeichen aus mindestens 3 von 4 Kategorien enthalten (Groß, Klein, Ziffern, Sonderzeichen).'
+ }
+
+ if (-not [string]::IsNullOrEmpty($SamAccountName)) {
+ $pwLower = $Password.ToLowerInvariant()
+ $samLower = $SamAccountName.ToLowerInvariant()
+
+ if ($pwLower -like "*${samLower}*") {
+ $errors += 'Passwort darf den Benutzernamen nicht enthalten.'
+ } else {
+ $minLen = 4
+ if ($samLower.Length -ge $minLen) {
+ for ($len = $minLen; $len -le $samLower.Length; $len++) {
+ for ($start = 0; $start -le $samLower.Length - $len; $start++) {
+ $sub = $samLower.Substring($start, $len)
+ if ($pwLower -like "*${sub}*") {
+ $errors += 'Passwort darf keine größeren Teile des Benutzernamens enthalten.'
+ break 2
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return $errors
+}
# Ensure ActiveDirectory module available
try {
Import-Module ActiveDirectory -ErrorAction Stop
@@ -52,6 +109,15 @@ $props = @{
if ($mail -and $mail -ne '') { $props['EmailAddress'] = $mail }
if ($ou -and $ou -ne '') { $props['Path'] = $ou }
+# Validate password before continuing
+$pwErrors = Test-PasswordRequirements -Password $pass -SamAccountName $sam
+if ($pwErrors.Count -gt 0) {
+ $result.message = 'Invalid password: ' + ($pwErrors -join ' | ')
+ $result.hint = $passwordHint
+ Write-Output ($result | ConvertTo-Json -Compress)
+ exit 1
+}
+
# Build secure password
$securePass = ConvertTo-SecureString $pass -AsPlainText -Force
$props['AccountPassword'] = $securePass
diff --git a/scripts/powershell/create_users_csv.ps1 b/scripts/powershell/create_users_csv.ps1
index bc430b3..009bd8f 100644
--- a/scripts/powershell/create_users_csv.ps1
+++ b/scripts/powershell/create_users_csv.ps1
@@ -65,6 +65,67 @@ foreach ($row in $items) {
$ou = $defaultOu
}
+ # Password hint text (German)
+ $passwordHint = @"
+Das sollten die Anforderungen an das Passwort sein:
+
+- mindestens 7 Zeichen
+- darf den Benutzer-/Accountnamen nicht enthalten (bzw. keine zu großen Teile davon)
+- muss Zeichen aus mindestens 3 von 4 Kategorien enthalten:
+ 1) Großbuchstaben (A–Z)
+ 2) Kleinbuchstaben (a–z)
+ 3) Ziffern (0–9)
+ 4) Sonderzeichen (alles, was kein Buchstabe/Zahl ist, z. B. ! ? # _ - . , usw.)
+"@
+
+ function Test-PasswordRequirements {
+ param(
+ [string]$Password,
+ [string]$SamAccountName
+ )
+
+ $errors = @()
+
+ if ([string]::IsNullOrEmpty($Password) -or $Password.Length -lt 7) {
+ $errors += 'Passwort muss mindestens 7 Zeichen lang sein.'
+ }
+
+ # Categories: uppercase, lowercase, digit, special
+ $categories = 0
+ if ($Password -match '[A-Z]') { $categories++ }
+ if ($Password -match '[a-z]') { $categories++ }
+ if ($Password -match '\d') { $categories++ }
+ if ($Password -match '[^A-Za-z0-9]') { $categories++ }
+ if ($categories -lt 3) {
+ $errors += 'Passwort muss Zeichen aus mindestens 3 von 4 Kategorien enthalten (Groß, Klein, Ziffern, Sonderzeichen).'
+ }
+
+ # Check for username inclusion or large parts of it (case-insensitive)
+ if (-not [string]::IsNullOrEmpty($SamAccountName)) {
+ $pwLower = $Password.ToLowerInvariant()
+ $samLower = $SamAccountName.ToLowerInvariant()
+
+ if ($pwLower -like "*${samLower}*") {
+ $errors += 'Passwort darf den Benutzernamen nicht enthalten.'
+ } else {
+ # Check for substrings of username of length >= 4
+ $minLen = 4
+ if ($samLower.Length -ge $minLen) {
+ for ($len = $minLen; $len -le $samLower.Length; $len++) {
+ for ($start = 0; $start -le $samLower.Length - $len; $start++) {
+ $sub = $samLower.Substring($start, $len)
+ if ($pwLower -like "*${sub}*") {
+ $errors += 'Passwort darf keine größeren Teile des Benutzernamens enthalten.'
+ break 2
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return $errors
+ }
if ([string]::IsNullOrWhiteSpace($sam) -or [string]::IsNullOrWhiteSpace($pass)) {
$results += @{ sam = $sam; success = $false; message = 'Missing samaccountname or password' }
@@ -72,8 +133,16 @@ foreach ($row in $items) {
continue
}
+ # Validate password according to requirements, also during dry run so issues are visible early
+ $pwErrors = Test-PasswordRequirements -Password $pass -SamAccountName $sam
+ if ($pwErrors.Count -gt 0) {
+ $results += @{ sam = $sam; success = $false; message = ('Invalid password: ' + ($pwErrors -join ' | ')); hint = $passwordHint }
+ $failCount++
+ continue
+ }
+
if ($dryRun) {
- $results += @{ sam = $sam; success = $true; message = 'DRY RUN: would create' }
+ $results += @{ sam = $sam; success = $true; message = 'DRY RUN: would create (password validated)' }
$successCount++
continue
}