develop #28

Merged
blaerf merged 83 commits from develop into main 2025-12-17 14:28:04 +00:00
6 changed files with 333 additions and 5 deletions
Showing only changes of commit d350130dba - Show all commits

View File

@ -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

View File

@ -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();

View File

@ -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 !== '') {

View File

@ -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>

View File

@ -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 (AZ)
2) Kleinbuchstaben (az)
3) Ziffern (09)
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

View File

@ -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 (AZ)
2) Kleinbuchstaben (az)
3) Ziffern (09)
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
}