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 }