develop #28

Merged
blaerf merged 83 commits from develop into main 2025-12-17 14:28:04 +00:00
8 changed files with 499 additions and 2 deletions
Showing only changes of commit 75828d8263 - Show all commits

View File

@ -81,6 +81,30 @@ Dieser Bereich muss von allen Entwicklern gelesen werden, bevor am Projekt gearb
---
## PowerShell Integration (Benutzererstellung)
Die Weboberfläche nutzt PowerShell-Skripte, um Active Directory Benutzer anzulegen. Damit dies funktioniert, sind folgende Voraussetzungen erforderlich:
- Der Webserver läuft auf Windows und PHP kann PowerShell ausführen (`powershell` oder `pwsh`).
- Die PowerShell-Module `ActiveDirectory` müssen installiert (RSAT) und verfügbar sein.
- Der Benutzer, unter dem der Webserver läuft, muss ausreichende Rechte besitzen, um `New-ADUser` und `Add-ADGroupMember` auszuführen.
- Im `config/config.php` kann `powershell.dry_run` auf `true` gesetzt werden, um Tests ohne Änderungen durchzuführen.
Konfigurationsoptionen (in `config/config.php`):
- `powershell.exe`: Name oder Pfad zur PowerShell-Executable (standard `powershell`).
- `powershell.script_dir`: Pfad zu den PowerShell-Skripten (standard `scripts/powershell`).
- `powershell.execution_policy`: Auszuführende ExecutionPolicy (z. B. `Bypass`).
- `powershell.dry_run`: Wenn `true`, werden keine echten AD-Änderungen durchgeführt; das Skript meldet nur, was es tun würde.
Die grundlegende Funktionalität wurde mit folgenden Komponenten implementiert:
- `public/api/create_user.php`: API-Endpoint zur Erstellung eines einzelnen Benutzers.
- `public/api/create_users_csv.php`: API-Endpoint zur Erstellung mehrerer Benutzer aus CSV.
- `scripts/powershell/create_user.ps1`: PowerShell-Skript zum Erstellen eines einzelnen Benutzers.
- `scripts/powershell/create_users_csv.ps1`: PowerShell-Skript zum Erstellen mehrerer Benutzer aus CSV.
Bitte testen zuerst mit `powershell.dry_run = true` und prüfen sie die resultierenden Meldungen in UI.
## Mitwirken
Wer etwas ändern oder erweitern möchte:

View File

@ -110,12 +110,36 @@ class UserManagementController
{
$viewPath = __DIR__ . '/../../public/views/createuser.php';
// Use session flash messages if available
$error = null;
$success = null;
if (session_status() !== PHP_SESSION_ACTIVE) {
@session_start();
}
if (isset($_SESSION['flash_error'])) {
$error = $_SESSION['flash_error'];
unset($_SESSION['flash_error']);
}
if (isset($_SESSION['flash_success'])) {
$success = $_SESSION['flash_success'];
unset($_SESSION['flash_success']);
}
$csvDetails = null;
if (isset($_SESSION['csv_details'])) {
$csvDetails = $_SESSION['csv_details'];
unset($_SESSION['csv_details']);
}
$powershellDryRun = $this->config['powershell']['dry_run'] ?? false;
return [
'view' => $viewPath,
'data' => [
'error' => null,
'success' => null,
'error' => $error,
'success' => $success,
'loginPage' => false,
'csvDetails' => $csvDetails,
'powershellDryRun' => $powershellDryRun,
],
'pageTitle' => 'Benutzer erstellen',
'activeMenu' => 'createuser',

View File

@ -60,4 +60,14 @@ return [
// Minimale Stufe: debug, info, warning, error
'min_level' => 'info',
],
'powershell' => [
// Executable name: 'powershell' on Windows, 'pwsh' for PowerShell core.
'exe' => 'powershell',
// Script directory where the PS1 scripts live (relative to config dir)
'script_dir' => __DIR__ . '/../scripts/powershell',
// Execution policy to pass to the PowerShell invocation
'execution_policy' => 'Bypass',
// For testing; if true, the scripts will run in dry-run mode (no real AD changes)
'dry_run' => false,
],
];

109
public/api/create_user.php Normal file
View File

@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
session_start();
// Load config
$config = require __DIR__ . '/../../config/config.php';
// Simple login check (same as index.php)
$sessionKey = $config['security']['session_key_user'] ?? 'admin_user';
if (!isset($_SESSION[$sessionKey])) {
header('Location: ../index.php?route=login');
exit;
}
// Only accept POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../index.php?route=createuser');
exit;
}
// Basic input validation
$sam = trim((string)($_POST['samaccountname'] ?? ''));
$display = trim((string)($_POST['displayname'] ?? ''));
$mail = trim((string)($_POST['mail'] ?? ''));
$pass = (string)($_POST['password'] ?? '');
$ou = trim((string)($_POST['ou'] ?? ''));
$groups = trim((string)($_POST['groups'] ?? ''));
if ($sam === '' || $pass === '') {
$_SESSION['flash_error'] = 'Anmeldename und Passwort sind erforderlich.';
header('Location: ../index.php?route=createuser');
exit;
}
// Build payload
$payload = [
'samaccountname' => $sam,
'displayname' => $display,
'mail' => $mail,
'password' => $pass,
'ou' => $ou,
'groups' => $groups,
'dry_run' => (bool)($config['powershell']['dry_run'] ?? false),
];
// Write payload to temp file
$tmpFile = tempnam(sys_get_temp_dir(), 'create_user_') . '.json';
file_put_contents($tmpFile, json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
// Build PS script path
$scriptDir = $config['powershell']['script_dir'] ?? __DIR__ . '/../../scripts/powershell';
$script = $scriptDir . DIRECTORY_SEPARATOR . 'create_user.ps1';
$exe = $config['powershell']['exe'] ?? 'powershell';
$executionPolicy = $config['powershell']['execution_policy'] ?? 'Bypass';
$cmd = sprintf(
'%s -NoProfile -NonInteractive -ExecutionPolicy %s -File "%s" -InputFile "%s"',
$exe,
$executionPolicy,
$script,
$tmpFile
);
// Execute and capture output and exit code
$output = [];
$returnVar = null;
if (!file_exists($script)) {
$_SESSION['flash_error'] = 'PowerShell-Skript nicht gefunden: ' . $script;
@unlink($tmpFile);
header('Location: ../index.php?route=createuser');
exit;
}
// Try to locate the PowerShell executable
$exePathCheck = shell_exec(sprintf('where %s 2>NUL', escapeshellarg($exe)));
if ($exePathCheck === null) {
// 'where' returns null when command fails; continue anyways, exec will fail if not found
}
exec($cmd . ' 2>&1', $output, $returnVar);
$json = implode("\n", $output);
@unlink($tmpFile);
// Try to parse JSON output
$result = null;
if ($json !== '') {
$decoded = json_decode($json, true);
if (is_array($decoded)) {
$result = $decoded;
}
}
if ($result === null) {
$_SESSION['flash_error'] = 'Unbekannter Fehler beim Ausführen des PowerShell-Skripts: ' . ($json ?: 'Keine Ausgabe');
header('Location: ../index.php?route=createuser');
exit;
}
if (!empty($result['success'])) {
$_SESSION['flash_success'] = $result['message'] ?? 'Benutzer erfolgreich erstellt.';
} else {
$_SESSION['flash_error'] = $result['message'] ?? 'Fehler beim Erstellen des Benutzers.';
}
header('Location: ../index.php?route=createuser');
exit;

View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
session_start();
// Load config
$config = require __DIR__ . '/../../config/config.php';
// Simple login check (same as index.php)
$sessionKey = $config['security']['session_key_user'] ?? 'admin_user';
if (!isset($_SESSION[$sessionKey])) {
header('Location: ../index.php?route=login');
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../index.php?route=createuser');
exit;
}
$delimiter = (string)($_POST['csvdelimiter'] ?? ',');
$hasHeader = (string)($_POST['hasHeader'] ?? '1');
// Two sources: uploaded file or csvcontent textarea
$csvContent = '';
if (!empty($_FILES['csvfile']['tmp_name']) && is_uploaded_file($_FILES['csvfile']['tmp_name'])) {
$csvContent = file_get_contents($_FILES['csvfile']['tmp_name']);
} else {
$csvContent = (string)($_POST['csvcontent'] ?? '');
}
if (trim($csvContent) === '') {
$_SESSION['flash_error'] = 'CSV ist leer. Bitte Datei auswählen oder Inhalt einfügen.';
header('Location: ../index.php?route=createuser');
exit;
}
// Write CSV to temp file
$tmpFile = tempnam(sys_get_temp_dir(), 'create_users_') . '.csv';
file_put_contents($tmpFile, $csvContent);
// Build payload with options
$payload = [
'input_file' => $tmpFile,
'delimiter' => $delimiter,
'has_header' => (bool)((int)$hasHeader),
'dry_run' => (bool)($config['powershell']['dry_run'] ?? false),
];
// Save options as JSON as the input to the PS script
$metaFile = tempnam(sys_get_temp_dir(), 'create_users_meta_') . '.json';
file_put_contents($metaFile, json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$scriptDir = $config['powershell']['script_dir'] ?? __DIR__ . '/../../scripts/powershell';
$script = $scriptDir . DIRECTORY_SEPARATOR . 'create_users_csv.ps1';
$exe = $config['powershell']['exe'] ?? 'powershell';
$executionPolicy = $config['powershell']['execution_policy'] ?? 'Bypass';
$cmd = sprintf(
'%s -NoProfile -NonInteractive -ExecutionPolicy %s -File "%s" -InputFile "%s"',
$exe,
$executionPolicy,
$script,
$metaFile
);
$output = [];
$returnVar = null;
if (!file_exists($script)) {
$_SESSION['flash_error'] = 'PowerShell-Skript nicht gefunden: ' . $script;
@unlink($tmpFile);
@unlink($metaFile);
header('Location: ../index.php?route=createuser');
exit;
}
exec($cmd . ' 2>&1', $output, $returnVar);
$json = implode("\n", $output);
@unlink($tmpFile);
@unlink($metaFile);
$result = null;
if ($json !== '') {
$decoded = json_decode($json, true);
if (is_array($decoded)) {
$result = $decoded;
}
}
if ($result === null) {
$_SESSION['flash_error'] = 'Unbekannter Fehler beim Ausführen des PowerShell-Skripts: ' . ($json ?: 'Keine Ausgabe');
header('Location: ../index.php?route=createuser');
exit;
}
if (!empty($result['success'])) {
$_SESSION['flash_success'] = $result['message'] ?? 'CSV verarbeitet.';
if (!empty($result['details'])) {
$_SESSION['csv_details'] = $result['details'];
}
} else {
$_SESSION['flash_error'] = $result['message'] ?? 'Fehler beim Verarbeiten der CSV.';
}
header('Location: ../index.php?route=createuser');
exit;

View File

@ -38,6 +38,12 @@ declare(strict_types=1);
</div>
<?php endif; ?>
<?php if (!empty($powershellDryRun) && $powershellDryRun === true): ?>
<div class="alert alert-warning" role="alert">
Die Anwendung ist im <strong>Dry-Run</strong>-Modus konfiguriert; PowerShell-Befehle werden nicht ausgeführt.
</div>
<?php endif; ?>
<p class="mb-4">Hier können Sie einzelne Active-Directory-Benutzer anlegen oder eine CSV-Datei hochladen, um mehrere Benutzer gleichzeitig zu erstellen. Sie können die CSV in der Vorschau bearbeiten bevor Sie die Erstellung auslösen.</p>
<div class="row">
@ -132,6 +138,40 @@ declare(strict_types=1);
</div>
</div>
<?php if (!empty($csvDetails) && is_array($csvDetails)): ?>
<div class="row mt-4">
<div class="col-12">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-secondary">CSV Verarbeitungsergebnisse</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm table-bordered">
<thead>
<tr>
<th>Anmeldename</th>
<th>Status</th>
<th>Hinweis</th>
</tr>
</thead>
<tbody>
<?php foreach ($csvDetails as $detail): ?>
<tr>
<td><?php echo htmlspecialchars((string)($detail['sam'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
<td><?php echo (!empty($detail['success'])) ? 'OK' : 'FEHLER'; ?></td>
<td><?php echo htmlspecialchars((string)($detail['message'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<?php endif; ?>
<div class="row">
<div class="col-12">
<div class="card shadow mb-4">

View File

@ -0,0 +1,86 @@
param(
[Parameter(Mandatory=$true)]
[string]$InputFile
)
# Read input JSON
try {
$json = Get-Content -Raw -Path $InputFile -ErrorAction Stop
$payload = $json | ConvertFrom-Json
} catch {
$err = $_.Exception.Message
Write-Output (@{ success = $false; message = "Failed to read/parse input JSON: $err" } | ConvertTo-Json -Compress)
exit 1
}
# Default result
$result = @{ success = $false; message = "Unspecified error" }
# Validate
if (-not $payload.samaccountname -or -not $payload.password) {
$result.message = "Required fields: samaccountname and password"
Write-Output ($result | ConvertTo-Json -Compress)
exit 1
}
# Convert to strings
$sam = [string]$payload.samaccountname
$display = [string]($payload.displayname)
$mail = [string]($payload.mail)
$pass = [string]$payload.password
$ou = [string]($payload.ou)
$groups = [string]($payload.groups)
$dryRun = [bool]($payload.dry_run -as [bool])
# Ensure ActiveDirectory module available
try {
Import-Module ActiveDirectory -ErrorAction Stop
} catch {
$result.message = "ActiveDirectory PowerShell module not available: $($_.Exception.Message)"
Write-Output ($result | ConvertTo-Json -Compress)
exit 1
}
# Build New-ADUser parameters
$props = @{
Name = if ($display -and $display -ne '') { $display } else { $sam }
SamAccountName = $sam
Enabled = $true
}
if ($mail -and $mail -ne '') { $props['EmailAddress'] = $mail }
if ($ou -and $ou -ne '') { $props['Path'] = $ou }
# Build secure password
$securePass = ConvertTo-SecureString $pass -AsPlainText -Force
$props['AccountPassword'] = $securePass
# Execute
if ($dryRun) {
$result.success = $true
$result.message = "DRY RUN: would create user $sam"
Write-Output ($result | ConvertTo-Json -Compress)
exit 0
}
try {
# Create the AD user
New-ADUser @props -ErrorAction Stop
# Add to groups, if provided
if ($groups -and $groups -ne '') {
$groupList = $groups -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }
foreach ($g in $groupList) {
Add-ADGroupMember -Identity $g -Members $sam -ErrorAction Stop
}
}
$result.success = $true
$result.message = "User $sam created successfully"
Write-Output ($result | ConvertTo-Json -Compress)
exit 0
} catch {
$result.message = "Error creating user $sam: $($_.Exception.Message)"
Write-Output ($result | ConvertTo-Json -Compress)
exit 1
}

View File

@ -0,0 +1,97 @@
param(
[Parameter(Mandatory=$true)]
[string]$InputFile
)
# Read meta JSON
try {
$json = Get-Content -Raw -Path $InputFile -ErrorAction Stop
$meta = $json | ConvertFrom-Json
} catch {
Write-Output (@{ success = $false; message = "Failed to read/parse meta JSON: $($_.Exception.Message)" } | ConvertTo-Json -Compress)
exit 1
}
$csvFile = [string]$meta.input_file
$delimiter = [string]($meta.delimiter ?? ',')
$hasHeader = [bool]($meta.has_header -as [bool])
$dryRun = [bool]($meta.dry_run -as [bool])
if (-not (Test-Path -Path $csvFile)) {
Write-Output (@{ success = $false; message = "CSV file not found: $csvFile" } | ConvertTo-Json -Compress)
exit 1
}
# Ensure ActiveDirectory module is available
try {
Import-Module ActiveDirectory -ErrorAction Stop
} catch {
Write-Output (@{ success = $false; message = "ActiveDirectory PowerShell module not available: $($_.Exception.Message)" } | ConvertTo-Json -Compress)
exit 1
}
# Read CSV
try {
if ($hasHeader) {
$items = Import-Csv -Path $csvFile -Delimiter $delimiter -ErrorAction Stop
} else {
# Use default headers
$headers = 'samaccountname','displayname','mail','password','ou','groups'
$items = Import-Csv -Path $csvFile -Delimiter $delimiter -Header $headers -ErrorAction Stop
}
} catch {
Write-Output (@{ success = $false; message = "Failed to parse CSV: $($_.Exception.Message)" } | ConvertTo-Json -Compress)
exit 1
}
$results = @()
$successCount = 0
$failCount = 0
foreach ($row in $items) {
$sam = $row.samaccountname
$display = $row.displayname
$mail = $row.mail
$pass = $row.password
$ou = $row.ou
$groups = $row.groups
if ([string]::IsNullOrWhiteSpace($sam) -or [string]::IsNullOrWhiteSpace($pass)) {
$results += @{ sam = $sam; success = $false; message = 'Missing samaccountname or password' }
$failCount++
continue
}
if ($dryRun) {
$results += @{ sam = $sam; success = $true; message = 'DRY RUN: would create' }
$successCount++
continue
}
try {
$props = @{ Name = if ($display -and $display -ne '') { $display } else { $sam }; SamAccountName = $sam; Enabled = $true }
if ($mail -and $mail -ne '') { $props['EmailAddress'] = $mail }
if ($ou -and $ou -ne '') { $props['Path'] = $ou }
$securePass = ConvertTo-SecureString $pass -AsPlainText -Force
$props['AccountPassword'] = $securePass
New-ADUser @props -ErrorAction Stop
if ($groups -and $groups -ne '') {
$groupList = $groups -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }
foreach ($g in $groupList) {
Add-ADGroupMember -Identity $g -Members $sam -ErrorAction Stop
}
}
$results += @{ sam = $sam; success = $true; message = 'Created' }
$successCount++
} catch {
$results += @{ sam = $sam; success = $false; message = $_.Exception.Message }
$failCount++
}
}
$output = @{ success = $failCount -eq 0; message = "Created $successCount users, $failCount failures"; details = $results }
Write-Output ($output | ConvertTo-Json -Compress)
exit 0