develop #28
@ -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
|
||||
|
||||
@ -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); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -280,6 +284,7 @@
|
||||
<div class="form-group">
|
||||
<label for="password">Passwort</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
<small class="text-muted password-hint">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.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@ -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 = '<strong>Hinweis:</strong> Einige Passwörter entsprechen nicht den Anforderungen. Bitte korrigieren Sie diese in der Tabelle bevor Sie importieren.';
|
||||
} else {
|
||||
previewInfo.innerHTML = '<strong>Hinweis:</strong> 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();
|
||||
|
||||
@ -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 !== '') {
|
||||
|
||||
@ -72,6 +72,7 @@ declare(strict_types=1);
|
||||
<div class="form-group">
|
||||
<label for="password">Passwort</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
<small class="form-text text-muted">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.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@ -81,6 +82,51 @@ declare(strict_types=1);
|
||||
|
||||
<button type="submit" class="btn btn-primary">Benutzer erstellen</button>
|
||||
</form>
|
||||
<script>
|
||||
(function () {
|
||||
const form = document.querySelector('form[action="/api/create_user.php"]');
|
||||
if (!form) return;
|
||||
function validatePassword(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;
|
||||
}
|
||||
|
||||
form.addEventListener('submit', function (e) {
|
||||
const sam = document.getElementById('samaccountname').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
const errs = validatePassword(password, sam);
|
||||
if (errs.length > 0) {
|
||||
e.preventDefault();
|
||||
alert('Passwort ungültig:\n' + errs.join('\n'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user