ldap/benutzer-und-gruppen-anzeigen (#4)

Co-authored-by: blaerf <blaerf@gmx.de>
Reviewed-on: https://git.eckertplayground.de/taarly/PHP_AdminTool_Projekt/pulls/4
This commit is contained in:
blaerf 2025-11-27 13:29:28 +00:00
parent 125d685ba5
commit 8b7359f319
8 changed files with 927 additions and 4 deletions

View File

@ -0,0 +1,153 @@
<?php
// Strenge Typprüfung für Parameter- und Rückgabetypen aktivieren.
declare(strict_types=1);
namespace App\Controllers;
use App\Services\Ldap\LdapDirectoryService;
/**
* Controller für die Benutzer- und Gruppenanzeige.
*
* Aufgaben:
* - holt über den LdapDirectoryService die Listen von Benutzern und Gruppen
* - behandelt technische Fehler und bereitet eine Fehlermeldung für die View auf
* - gibt die Daten an eine View-Datei (public/views/users.php) weiter
*
* WICHTIG:
* - Es werden aktuell nur Daten angezeigt (Read-only).
* - Es findet keine Änderung im Active Directory statt.
*/
class UserManagementController
{
/** @var array<string, mixed> Vollständige Anwendungskonfiguration (aus config.php) */
private array $config;
/** @var LdapDirectoryService Service für das Lesen von Benutzern und Gruppen aus dem LDAP/AD */
private LdapDirectoryService $directoryService;
/**
* @param array<string, mixed> $config Vollständige Konfiguration aus config.php
*/
public function __construct(array $config)
{
// Komplette Konfiguration speichern (falls später weitere Werte benötigt werden).
$this->config = $config;
// LDAP-Konfiguration aus der Gesamt-Konfiguration herausziehen.
$ldapConfig = $config['ldap'] ?? [];
// Directory-Service initialisieren, der die eigentliche LDAP-Arbeit übernimmt.
$this->directoryService = new LdapDirectoryService($ldapConfig);
}
/**
* Zeigt Benutzer- und Gruppenliste an.
* Wird typischerweise über die Route "users" (index.php?route=users) aufgerufen.
*/
public function show(): void
{
// Standardwerte für die View-Variablen vorbereiten.
$error = null;
$users = [];
$groups = [];
try {
// Benutzer- und Gruppenlisten aus dem AD laden.
$users = $this->directoryService->getUsers();
$groups = $this->directoryService->getGroups();
} catch (\Throwable $exception) {
// Sämtliche technischen Fehler (z. B. Verbindungs- oder Konfigurationsprobleme)
// werden hier in eine für den Benutzer lesbare Fehlermeldung übersetzt.
$error = 'Fehler beim Laden von Benutzern/Gruppen: ' . $exception->getMessage();
}
// Pfad zur eigentlichen View-Datei bestimmen.
$viewPath = __DIR__ . '/../../public/views/users.php';
// Falls die View-Datei (noch) nicht existiert, Fallback-Ausgabe verwenden.
if (file_exists($viewPath) === false) {
$this->renderInline($users, $groups, $error);
return;
}
// Variablen $users, $groups, $error stehen in der View zur Verfügung,
// weil sie im aktuellen Scope definiert sind.
require $viewPath;
}
/**
* Fallback-Ausgabe, falls noch keine View-Datei existiert.
*
* @param array<int, array<string, string>> $users Liste der Benutzer-Datensätze
* @param array<int, array<string, string>> $groups Liste der Gruppen-Datensätze
* @param string|null $error Fehlermeldung (falls vorhanden)
*/
private function renderInline(array $users, array $groups, ?string $error): void
{
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>AD Admin Tool Benutzer &amp; Gruppen</title>
</head>
<body>
<h1>Benutzer &amp; Gruppen</h1>
<?php if ($error !== null): ?>
<!-- Fehlermeldung ausgeben, HTML-sicher maskiert -->
<p style="color: red;"><?php echo htmlspecialchars($error, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></p>
<?php endif; ?>
<h2>Benutzer</h2>
<table border="1" cellpadding="4" cellspacing="0">
<thead>
<tr>
<th>Benutzername (sAMAccountName)</th>
<th>Anzeigename</th>
<th>E-Mail</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<tr>
<td><?php echo htmlspecialchars($user['samaccountname'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
<td><?php echo htmlspecialchars($user['displayname'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
<td><?php echo htmlspecialchars($user['mail'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<h2>Gruppen</h2>
<table border="1" cellpadding="4" cellspacing="0">
<thead>
<tr>
<th>Gruppenname (sAMAccountName)</th>
<th>CN</th>
<th>Beschreibung</th>
</tr>
</thead>
<tbody>
<?php foreach ($groups as $group): ?>
<tr>
<td><?php echo htmlspecialchars($group['samaccountname'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
<td><?php echo htmlspecialchars($group['cn'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
<td><?php echo htmlspecialchars($group['description'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p>
<a href="index.php?route=dashboard">Zurück zum Dashboard</a> |
<a href="index.php?route=logout">Logout</a>
</p>
</body>
</html>
<?php
}
}

View File

@ -1,21 +1,37 @@
<?php <?php
// Strenge Typprüfung für Parameter- und Rückgabetypen aktivieren.
declare(strict_types=1); declare(strict_types=1);
namespace App\Services\Ldap; namespace App\Services\Ldap;
use RuntimeException; use RuntimeException;
/**
* Hilfsklasse zum Aufbau einer LDAP/LDAPS-Verbindung.
*
* Aufgaben:
* - liest Server, Port und Timeout aus der LDAP-Konfiguration
* - erstellt eine LDAP-Verbindung
* - setzt die notwendigen Optionen (Protokollversion, Netzwerk-Timeout)
*
* Wichtig:
* - Diese Klasse führt KEIN ldap_bind durch.
* - Das Bind (mit Benutzer- oder Service-Konto) erfolgt in den Fach-Services
* wie LdapAuthService oder LdapDirectoryService.
*/
class LdapConnectionHelper class LdapConnectionHelper
{ {
/** @var array<string, mixed> */ /** @var array<string, mixed> LDAP-spezifische Konfiguration (server, port, timeout, etc.) */
private array $config; private array $config;
/** /**
* @param array<string, mixed> $ldapConfig * @param array<string, mixed> $ldapConfig Teilbereich "ldap" aus der config.php
*/ */
public function __construct(array $ldapConfig) public function __construct(array $ldapConfig)
{ {
// LDAP-Konfiguration in der Instanz speichern.
// Dadurch steht sie in allen Methoden dieser Klasse zur Verfügung.
$this->config = $ldapConfig; $this->config = $ldapConfig;
} }
@ -24,26 +40,43 @@ class LdapConnectionHelper
* aber ohne Bind. Den Bind führen die aufrufenden Services durch. * aber ohne Bind. Den Bind führen die aufrufenden Services durch.
* *
* @return resource LDAP-Verbindungs-Handle * @return resource LDAP-Verbindungs-Handle
*
* @throws RuntimeException wenn der Server nicht konfiguriert ist oder die Verbindung scheitert
*/ */
public function createConnection() public function createConnection()
{ {
// Server, Port und Timeout aus der Konfiguration lesen.
// Die Null-Koaleszenz-Operatoren (??) setzen Standardwerte, falls Keys fehlen.
$server = (string)($this->config['server'] ?? ''); $server = (string)($this->config['server'] ?? '');
$port = (int)($this->config['port'] ?? 636); $port = (int)($this->config['port'] ?? 636);
$timeout = (int)($this->config['timeout'] ?? 5); $timeout = (int)($this->config['timeout'] ?? 5);
// Ohne Server-Adresse kann keine Verbindung aufgebaut werden.
if ($server === '') { if ($server === '') {
throw new RuntimeException('LDAP-Konfiguration ist unvollständig (server fehlt).'); throw new RuntimeException('LDAP-Konfiguration ist unvollständig (server fehlt).');
} }
// Verbindung zum LDAP/AD-Server herstellen.
// ldap_connect liefert entweder ein Verbindungs-Handle (Resource) oder false.
$connection = ldap_connect($server, $port); $connection = ldap_connect($server, $port);
// Wenn keine Verbindung aufgebaut werden konnte, Exception werfen.
if ($connection === false) { if ($connection === false) {
throw new RuntimeException('LDAP-Verbindung konnte nicht aufgebaut werden.'); throw new RuntimeException('LDAP-Verbindung konnte nicht aufgebaut werden.');
} }
// LDAP-Version auf 3 setzen, dies ist die gängige Standardversion.
ldap_set_option($connection, LDAP_OPT_PROTOCOL_VERSION, 3); ldap_set_option($connection, LDAP_OPT_PROTOCOL_VERSION, 3);
// Netzwerk-Timeout setzen, damit Anfragen nicht ewig hängen bleiben.
ldap_set_option($connection, LDAP_OPT_NETWORK_TIMEOUT, $timeout); ldap_set_option($connection, LDAP_OPT_NETWORK_TIMEOUT, $timeout);
// Referral-Following deaktivieren (0 = aus).
// Das verhindert, dass der Client automatisch zu anderen LDAP-Servern weitergeleitet wird.
ldap_set_option($connection, LDAP_OPT_REFERRALS, 0);
// Verbindungs-Handle an aufrufende Services zurückgeben.
// Dort wird dann der eigentliche ldap_bind durchgeführt.
return $connection; return $connection;
} }
} }

View File

@ -0,0 +1,203 @@
<?php
// Strenge Typprüfung für Parameter- und Rückgabetypen aktivieren.
declare(strict_types=1);
namespace App\Services\Ldap;
use RuntimeException;
/**
* Service zum Lesen von Objekten aus dem Active Directory.
*
* Aktueller Umfang:
* - Liste von Benutzern (sAMAccountName, displayName, mail)
* - Liste von Gruppen (sAMAccountName, cn, description)
*
* Technische Details:
* - Verwendet ein technisches Konto (bind_dn + bind_password) für Lesezugriffe
* - Nutzt LdapConnectionHelper zum Aufbau der Verbindung
*/
class LdapDirectoryService
{
/** @var array<string, mixed> LDAP-Konfiguration (inkl. base_dn, bind_dn, bind_password) */
private array $config;
/** @var LdapConnectionHelper Zentrale Hilfsklasse für den Aufbau von LDAP-Verbindungen */
private LdapConnectionHelper $connectionHelper;
/**
* @param array<string, mixed> $ldapConfig Teilbereich "ldap" aus der config.php
*/
public function __construct(array $ldapConfig)
{
// Vollständige LDAP-Konfiguration speichern.
$this->config = $ldapConfig;
// Gemeinsamen Verbindungs-Helper initialisieren.
// Dieser kümmert sich um ldap_connect und die grundlegenden Optionen.
$this->connectionHelper = new LdapConnectionHelper($ldapConfig);
}
/**
* Stellt eine LDAP-Verbindung her und bindet sich mit dem technischen Konto.
*
* @return resource LDAP-Verbindungs-Handle
*
* @throws RuntimeException wenn Bind-Daten fehlen oder der Bind fehlschlägt
*/
private function connect()
{
// Technischen Bind-DN (Service-Account) aus der Konfiguration lesen.
$bindDn = (string)($this->config['bind_dn'] ?? '');
$bindPassword = (string)($this->config['bind_password'] ?? '');
// Ohne Bind-DN oder Passwort kann kein technischer Bind durchgeführt werden.
if ($bindDn === '' || $bindPassword === '') {
throw new RuntimeException('LDAP-Binddaten für technisches Konto sind nicht konfiguriert.');
}
// Verbindung über den zentralen Helper erstellen (Server, Port, Optionen).
$connection = $this->connectionHelper->createConnection();
// Mit dem technischen Konto binden, um Lesezugriffe durchführen zu können.
// Das @-Zeichen unterdrückt PHP-Warnings bei Fehlversuchen.
if (@ldap_bind($connection, $bindDn, $bindPassword) !== true) {
// Bei fehlgeschlagenem Bind Verbindung wieder schließen.
ldap_unbind($connection);
throw new RuntimeException('LDAP-Bind mit technischem Konto ist fehlgeschlagen.');
}
// Erfolgreich gebundene Verbindung zurückgeben.
return $connection;
}
/**
* Liefert eine Liste von Benutzern aus dem AD.
*
* @return array<int, array<string, string>> Liste von Benutzer-Datensätzen
* [
* [
* 'samaccountname' => 'user1',
* 'displayname' => 'User Eins',
* 'mail' => 'user1@example.local',
* ],
* ...
* ]
*
* @throws RuntimeException bei Konfigurations- oder Verbindungsproblemen
*/
public function getUsers(): array
{
// Base-DN ist der Startpunkt der Suche im Verzeichnisbaum (z. B. DC=...,DC=...).
$baseDn = (string)($this->config['base_dn'] ?? '');
if ($baseDn === '') {
throw new RuntimeException('LDAP base_dn für Benutzersuche ist nicht konfiguriert.');
}
// Verbindung mit technischem Bind herstellen.
$connection = $this->connect();
// Standard-Filter für Benutzer in AD:
// - objectClass=user
// - objectCategory=person
$filter = '(&(objectClass=user)(objectCategory=person))';
// Nur relevante Attribute auslesen, um Datenmenge klein zu halten.
$attributes = ['sAMAccountName', 'displayName', 'mail'];
// LDAP-Suche unterhalb des Base-DN durchführen.
$search = @ldap_search($connection, $baseDn, $filter, $attributes);
if ($search === false) {
// Bei einem Fehler die Verbindung schließen und Exception werfen.
ldap_unbind($connection);
throw new RuntimeException('LDAP-Suche nach Benutzern ist fehlgeschlagen.');
}
// Suchergebnisse in ein PHP-Array umwandeln.
$entries = ldap_get_entries($connection, $search);
// Verbindung wieder schließen, da sie nicht mehr benötigt wird.
ldap_unbind($connection);
// Ergebnisliste für die View vorbereiten.
$users = [];
// Wenn die Struktur unerwartet ist, leere Liste zurückgeben.
if (!is_array($entries) || !isset($entries['count'])) {
return $users;
}
// Jeden einzelnen Eintrag aus dem Ergebnis verarbeiten.
for ($i = 0; $i < $entries['count']; $i++) {
$entry = $entries[$i];
// Attribute sind im Ergebnis verschachtelt, daher Zugriff über [...][0].
$users[] = [
'samaccountname' => isset($entry['samaccountname'][0]) ? (string)$entry['samaccountname'][0] : '',
'displayname' => isset($entry['displayname'][0]) ? (string)$entry['displayname'][0] : '',
'mail' => isset($entry['mail'][0]) ? (string)$entry['mail'][0] : '',
];
}
return $users;
}
/**
* Liefert eine Liste von Gruppen aus dem AD.
*
* @return array<int, array<string, string>> Liste von Gruppen-Datensätzen
*
* @throws RuntimeException bei Konfigurations- oder Verbindungsproblemen
*/
public function getGroups(): array
{
// Base-DN für die Gruppensuche aus der Konfiguration lesen.
$baseDn = (string)($this->config['base_dn'] ?? '');
if ($baseDn === '') {
throw new RuntimeException('LDAP base_dn für Gruppensuche ist nicht konfiguriert.');
}
// Verbindung mit technischem Bind herstellen.
$connection = $this->connect();
// Filter für Gruppenobjekte im AD.
$filter = '(objectClass=group)';
// Attribute, die für die Darstellung relevant sind.
$attributes = ['sAMAccountName', 'cn', 'description'];
// LDAP-Suche ausführen.
$search = @ldap_search($connection, $baseDn, $filter, $attributes);
if ($search === false) {
ldap_unbind($connection);
throw new RuntimeException('LDAP-Suche nach Gruppen ist fehlgeschlagen.');
}
// Ergebnisse holen.
$entries = ldap_get_entries($connection, $search);
// Verbindung schließen.
ldap_unbind($connection);
$groups = [];
// Struktur prüfen, leere Liste zurückgeben, falls unerwartet.
if (!is_array($entries) || !isset($entries['count'])) {
return $groups;
}
// Alle Einträge in ein flacheres Array-Format transformieren.
for ($i = 0; $i < $entries['count']; $i++) {
$entry = $entries[$i];
$groups[] = [
'samaccountname' => isset($entry['samaccountname'][0]) ? (string)$entry['samaccountname'][0] : '',
'cn' => isset($entry['cn'][0]) ? (string)$entry['cn'][0] : '',
'description' => isset($entry['description'][0]) ? (string)$entry['description'][0] : '',
];
}
return $groups;
}
}

View File

@ -18,6 +18,9 @@ return [
// Optional: Timeout in Sekunden // Optional: Timeout in Sekunden
'timeout' => 5, 'timeout' => 5,
'bind_dn' => 'CN=Service IIS,OU=WebAppUsers,DC=ITFA-PROJ-DOM,DC=local',
'bind_password' => '$7aE!R$l$D!p1Q9l458K8@O6&',
], ],
'security' => [ 'security' => [

View File

@ -47,6 +47,7 @@ $config = require $configPath;
use App\Controllers\AuthController; use App\Controllers\AuthController;
use App\Controllers\DashboardController; use App\Controllers\DashboardController;
use App\Controllers\UserManagementController;
// Hilfsfunktion für geschützte Routen // Hilfsfunktion für geschützte Routen
function requireLogin(array $config): void function requireLogin(array $config): void
@ -71,6 +72,9 @@ $authController = new AuthController($config);
// Neue Instanz der Klasse DashboardController erstellen (wird bei Bedarf über den Autoloader geladen). // Neue Instanz der Klasse DashboardController erstellen (wird bei Bedarf über den Autoloader geladen).
$dashboardController = new DashboardController($config); $dashboardController = new DashboardController($config);
// Neue Instanz der Klasse UserManagmentController erstellen (wird bei Bedarf über den Autoloader geladen).
$userManagementController = new UserManagementController($config);
// Anhand des Routing-Ziels (route) entscheiden, welcher Code ausgeführt wird. // Anhand des Routing-Ziels (route) entscheiden, welcher Code ausgeführt wird.
switch ($route) { switch ($route) {
case 'login': case 'login':
@ -94,6 +98,11 @@ switch ($route) {
$dashboardController->show(); $dashboardController->show();
break; break;
case 'users':
requireLogin($config);
$userManagementController->show();
break;
default: default:
http_response_code(404); http_response_code(404);
echo 'Route nicht gefunden.'; echo 'Route nicht gefunden.';

View File

@ -1,4 +1,6 @@
// Call the dataTables jQuery plugin // Call the dataTables jQuery plugin
$(document).ready(function() { $(document).ready(function() {
$('#dataTable').DataTable(); $('#dataTable').DataTable();
$('#usersTable').DataTable();
$('#groupsTable').DataTable();
}); });

520
public/views/users.php Normal file
View File

@ -0,0 +1,520 @@
<?php
declare(strict_types=1);
/** @var array<int, array<string, string>> $users */
/** @var array<int, array<string, string>> $groups */
/** @var string|null $error */
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>SB Admin 2 - Tables</title>
<!-- Custom fonts for this template -->
<link href="../vendor/fontawesome-free/css/all.min.css" rel="stylesheet" type="text/css">
<link
href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i"
rel="stylesheet">
<!-- Custom styles for this template -->
<link href="../css/sb-admin-2.min.css" rel="stylesheet">
<!-- Custom styles for this page -->
<link href="../vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
</head>
<body id="page-top">
<!-- Page Wrapper -->
<div id="wrapper">
<!-- Sidebar -->
<ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">
<!-- Sidebar - Brand -->
<a class="sidebar-brand d-flex align-items-center justify-content-center" href="../views/dashboard.php">
<div class="sidebar-brand-icon rotate-n-15">
<i class="fas fa-laugh-wink"></i>
</div>
<div class="sidebar-brand-text mx-3">SB Admin <sup>2</sup></div>
</a>
<!-- Divider -->
<hr class="sidebar-divider my-0">
<!-- Nav Item - Dashboard -->
<li class="nav-item">
<a class="nav-link" href="../views/dashboard.php">
<i class="fas fa-fw fa-tachometer-alt"></i>
<span>Dashboard</span></a>
</li>
<!-- Divider -->
<hr class="sidebar-divider">
<!-- Heading -->
<div class="sidebar-heading">
Interface
</div>
<!-- Nav Item - Pages Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseTwo"
aria-expanded="true" aria-controls="collapseTwo">
<i class="fas fa-fw fa-cog"></i>
<span>Components</span>
</a>
<div id="collapseTwo" class="collapse" aria-labelledby="headingTwo" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<h6 class="collapse-header">Custom Components:</h6>
<a class="collapse-item" href="../buttons.php">Buttons</a>
<a class="collapse-item" href="../cards.php">Cards</a>
</div>
</div>
</li>
<!-- Nav Item - Utilities Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseUtilities"
aria-expanded="true" aria-controls="collapseUtilities">
<i class="fas fa-fw fa-wrench"></i>
<span>Utilities</span>
</a>
<div id="collapseUtilities" class="collapse" aria-labelledby="headingUtilities"
data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<h6 class="collapse-header">Custom Utilities:</h6>
<a class="collapse-item" href="../utilities-color.php">Colors</a>
<a class="collapse-item" href="../utilities-border.php">Borders</a>
<a class="collapse-item" href="../utilities-animation.php">Animations</a>
<a class="collapse-item" href="../utilities-other.php">Other</a>
</div>
</div>
</li>
<!-- Divider -->
<hr class="sidebar-divider">
<!-- Heading -->
<div class="sidebar-heading">
Addons
</div>
<!-- Nav Item - Pages Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapsePages"
aria-expanded="true" aria-controls="collapsePages">
<i class="fas fa-fw fa-folder"></i>
<span>Pages</span>
</a>
<div id="collapsePages" class="collapse" aria-labelledby="headingPages" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<h6 class="collapse-header">Login Screens:</h6>
<a class="collapse-item" href="login.php">Login</a>
<a class="collapse-item" href="../register.php">Register</a>
<a class="collapse-item" href="../forgot-password.php">Forgot Password</a>
<div class="collapse-divider"></div>
<h6 class="collapse-header">Other Pages:</h6>
<a class="collapse-item" href="../404.php">404 Page</a>
<a class="collapse-item" href="../blank.php">Blank Page</a>
</div>
</div>
</li>
<!-- Nav Item - Charts -->
<li class="nav-item">
<a class="nav-link" href="../charts.php">
<i class="fas fa-fw fa-chart-area"></i>
<span>Charts</span></a>
</li>
<!-- Nav Item - Tables -->
<li class="nav-item active">
<a class="nav-link" href="../tables.html">
<i class="fas fa-fw fa-table"></i>
<span>Tables</span></a>
</li>
<!-- Divider -->
<hr class="sidebar-divider d-none d-md-block">
<!-- Sidebar Toggler (Sidebar) -->
<div class="text-center d-none d-md-inline">
<button class="rounded-circle border-0" id="sidebarToggle"></button>
</div>
</ul>
<!-- End of Sidebar -->
<!-- Content Wrapper -->
<div id="content-wrapper" class="d-flex flex-column">
<!-- Main Content -->
<div id="content">
<!-- Topbar -->
<nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
<!-- Sidebar Toggle (Topbar) -->
<form class="form-inline">
<button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
<i class="fa fa-bars"></i>
</button>
</form>
<!-- Topbar Search -->
<form
class="d-none d-sm-inline-block form-inline mr-auto ml-md-3 my-2 my-md-0 mw-100 navbar-search">
<div class="input-group">
<input type="text" class="form-control bg-light border-0 small" placeholder="Search for..."
aria-label="Search" aria-describedby="basic-addon2">
<div class="input-group-append">
<button class="btn btn-primary" type="button">
<i class="fas fa-search fa-sm"></i>
</button>
</div>
</div>
</form>
<!-- Topbar Navbar -->
<ul class="navbar-nav ml-auto">
<!-- Nav Item - Search Dropdown (Visible Only XS) -->
<li class="nav-item dropdown no-arrow d-sm-none">
<a class="nav-link dropdown-toggle" href="#" id="searchDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-search fa-fw"></i>
</a>
<!-- Dropdown - Messages -->
<div class="dropdown-menu dropdown-menu-right p-3 shadow animated--grow-in"
aria-labelledby="searchDropdown">
<form class="form-inline mr-auto w-100 navbar-search">
<div class="input-group">
<input type="text" class="form-control bg-light border-0 small"
placeholder="Search for..." aria-label="Search"
aria-describedby="basic-addon2">
<div class="input-group-append">
<button class="btn btn-primary" type="button">
<i class="fas fa-search fa-sm"></i>
</button>
</div>
</div>
</form>
</div>
</li>
<!-- Nav Item - Alerts -->
<li class="nav-item dropdown no-arrow mx-1">
<a class="nav-link dropdown-toggle" href="#" id="alertsDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-bell fa-fw"></i>
<!-- Counter - Alerts -->
<span class="badge badge-danger badge-counter">3+</span>
</a>
<!-- Dropdown - Alerts -->
<div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in"
aria-labelledby="alertsDropdown">
<h6 class="dropdown-header">
Alerts Center
</h6>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="mr-3">
<div class="icon-circle bg-primary">
<i class="fas fa-file-alt text-white"></i>
</div>
</div>
<div>
<div class="small text-gray-500">December 12, 2019</div>
<span class="font-weight-bold">A new monthly report is ready to download!</span>
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="mr-3">
<div class="icon-circle bg-success">
<i class="fas fa-donate text-white"></i>
</div>
</div>
<div>
<div class="small text-gray-500">December 7, 2019</div>
$290.29 has been deposited into your account!
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="mr-3">
<div class="icon-circle bg-warning">
<i class="fas fa-exclamation-triangle text-white"></i>
</div>
</div>
<div>
<div class="small text-gray-500">December 2, 2019</div>
Spending Alert: We've noticed unusually high spending for your account.
</div>
</a>
<a class="dropdown-item text-center small text-gray-500" href="#">Show All Alerts</a>
</div>
</li>
<!-- Nav Item - Messages -->
<li class="nav-item dropdown no-arrow mx-1">
<a class="nav-link dropdown-toggle" href="#" id="messagesDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-envelope fa-fw"></i>
<!-- Counter - Messages -->
<span class="badge badge-danger badge-counter">7</span>
</a>
<!-- Dropdown - Messages -->
<div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in"
aria-labelledby="messagesDropdown">
<h6 class="dropdown-header">
Message Center
</h6>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="dropdown-list-image mr-3">
<img class="rounded-circle" src="../images/undraw_profile_1.svg"
alt="...">
<div class="status-indicator bg-success"></div>
</div>
<div class="font-weight-bold">
<div class="text-truncate">Hi there! I am wondering if you can help me with a
problem I've been having.</div>
<div class="small text-gray-500">Emily Fowler · 58m</div>
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="dropdown-list-image mr-3">
<img class="rounded-circle" src="../images/undraw_profile_2.svg"
alt="...">
<div class="status-indicator"></div>
</div>
<div>
<div class="text-truncate">I have the photos that you ordered last month, how
would you like them sent to you?</div>
<div class="small text-gray-500">Jae Chun · 1d</div>
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="dropdown-list-image mr-3">
<img class="rounded-circle" src="../images/undraw_profile_3.svg"
alt="...">
<div class="status-indicator bg-warning"></div>
</div>
<div>
<div class="text-truncate">Last month's report looks great, I am very happy with
the progress so far, keep up the good work!</div>
<div class="small text-gray-500">Morgan Alvarez · 2d</div>
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="dropdown-list-image mr-3">
<img class="rounded-circle" src="https://source.unsplash.com/Mv9hjnEUHR4/60x60"
alt="...">
<div class="status-indicator bg-success"></div>
</div>
<div>
<div class="text-truncate">Am I a good boy? The reason I ask is because someone
told me that people say this to all dogs, even if they aren't good...</div>
<div class="small text-gray-500">Chicken the Dog · 2w</div>
</div>
</a>
<a class="dropdown-item text-center small text-gray-500" href="#">Read More Messages</a>
</div>
</li>
<div class="topbar-divider d-none d-sm-block"></div>
<!-- Nav Item - User Information -->
<li class="nav-item dropdown no-arrow">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mr-2 d-none d-lg-inline text-gray-600 small">Douglas McGee</span>
<img class="img-profile rounded-circle"
src="../images/undraw_profile.svg">
</a>
<!-- Dropdown - User Information -->
<div class="dropdown-menu dropdown-menu-right shadow animated--grow-in"
aria-labelledby="userDropdown">
<a class="dropdown-item" href="#">
<i class="fas fa-user fa-sm fa-fw mr-2 text-gray-400"></i>
Profile
</a>
<a class="dropdown-item" href="#">
<i class="fas fa-cogs fa-sm fa-fw mr-2 text-gray-400"></i>
Settings
</a>
<a class="dropdown-item" href="#">
<i class="fas fa-list fa-sm fa-fw mr-2 text-gray-400"></i>
Activity Log
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" data-toggle="modal" data-target="#logoutModal">
<i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
Logout
</a>
</div>
</li>
</ul>
</nav>
<!-- End of Topbar -->
<!-- Begin Page Content -->
<div class="container-fluid">
<!-- Page Heading -->
<h1 class="h3 mb-2 text-gray-800">Benutzer & Gruppen</h1>
<?php if ($error !== null): ?>
<p class="error">
<?php echo htmlspecialchars($error, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
</p>
<?php endif; ?>
<p class="mb-4">DataTables is a third party plugin that is used to generate the demo table below.
For more information about DataTables, please visit the <a target="_blank"
href="https://datatables.net">official DataTables documentation</a>.</p>
<!-- DataTales Example -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Benutzer</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" id="usersTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>Anmeldename</th>
<th>Anzeigename</th>
<th>E-Mail</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Anmeldename</th>
<th>Anzeigename</th>
<th>E-Mail</th>
</tr>
</tfoot>
<tbody>
<?php foreach ($users as $user): ?>
<tr>
<td><?php echo htmlspecialchars($user['samaccountname'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
<td><?php echo htmlspecialchars($user['displayname'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
<td><?php echo htmlspecialchars($user['mail'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Gruppen</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" id="groupsTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>Gruppenname</th>
<th>CN</th>
<th>Beschreibung</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Gruppenname</th>
<th>CN</th>
<th>Beschreibung</th>
</tr>
</tfoot>
<tbody>
<?php foreach ($groups as $group): ?>
<tr>
<td><?php echo htmlspecialchars($group['samaccountname'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
<td><?php echo htmlspecialchars($group['cn'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
<td><?php echo htmlspecialchars($group['description'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- /.container-fluid -->
</div>
<!-- End of Main Content -->
<!-- Footer -->
<footer class="sticky-footer bg-white">
<div class="container my-auto">
<div class="copyright text-center my-auto">
<span>Copyright &copy; Your Website 2020</span>
</div>
</div>
</footer>
<!-- End of Footer -->
</div>
<!-- End of Content Wrapper -->
</div>
<!-- End of Page Wrapper -->
<!-- Scroll to Top Button-->
<a class="scroll-to-top rounded" href="#page-top">
<i class="fas fa-angle-up"></i>
</a>
<!-- Logout Modal-->
<div class="modal fade" id="logoutModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Ready to Leave?</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">Select "Logout" below if you are ready to end your current session.</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
<a class="btn btn-primary" href="login.php">Logout</a>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript-->
<script src="../vendor/jquery/jquery.min.js"></script>
<script src="../vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
<!-- Core plugin JavaScript-->
<script src="../vendor/jquery-easing/jquery.easing.min.js"></script>
<!-- Custom scripts for all pages-->
<script src="../js/sb-admin-2.min.js"></script>
<!-- Page level plugins -->
<script src="../vendor/datatables/jquery.dataTables.min.js"></script>
<script src="../vendor/datatables/dataTables.bootstrap4.min.js"></script>
<!-- Page level custom scripts -->
<script src="../js/demo/datatables-demo.js"></script>
</body>
</html>