Compare commits

...

8 Commits

Author SHA1 Message Date
ff6cd38c8d update README 2025-03-27 21:03:55 +01:00
9e21bdc135 add doc 2025-03-27 21:00:25 +01:00
513b7563b6 refactor 2025-03-27 20:38:02 +01:00
1521d8dec2 add README 2025-03-27 20:36:51 +01:00
2a2a96a7b4 working ldaps 2025-03-27 20:28:42 +01:00
4ebfd046de ldaps aaaaaaaaaaaaaaaaaaaaaaa 2025-03-26 23:06:14 +01:00
45b9cc7dfa display attempts to admin 2025-03-26 20:48:39 +01:00
db2079e0b3 update TODO 2025-03-26 19:43:29 +01:00
15 changed files with 548 additions and 66 deletions

View File

@@ -0,0 +1,31 @@
# PHP LDAP
## Configuration
### AD
Par défaut, l'application se connecte au serveur AD à l'adresse `woodywood.local`
### Base de données
Par défaut, l'application se connecte à la base de données locale
avec l'utilisateur `root` et le mot de passe `4321`
## Captures d'écran
Page d'authentification :
[[screenshots/auth.png]]
Page d'erreur :
[[screenshots/error.png]]
Page utilisateur :
[[screenshots/success.png]]
Page admin (en se connectant avec l'utilisateur Administrateur):
[[screenshots/admin.png]]

22
css/dataTables.css Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,15 +0,0 @@
<?php
$host = 'localhost'; // ou l'adresse IP du serveur MariaDB
$dbname = 'mysql'; // nom de votre base de donn<6E>es
$username = 'root'; // nom d'utilisateur MariaDB
$password = '4321'; // mot de passe pour l'utilisateur
try {
// Cr<43>ation d'une instance PDO pour la connexion <20> la base de donn<6E>es
$pdo = new PDO("mysql:host=$host;dbname=$dbname", $username, $password);
// Configuration du mode d'erreur de PDO pour les exceptions
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
echo "Connexion réussie <20> MariaDB avec PDO!";
} catch (PDOException $e) {
echo "<EFBFBD>chec de la connexion : " . $e->getMessage();
}
?>

View File

@@ -1,3 +1,27 @@
<?php
phpinfo();
?>
if (!isset($_POST["domain"]) || !isset($_POST["user"]) || !isset($_POST["password"])) {
require_once "templates/login_form.html";
exit;
}
$domain = rtrim($_POST["domain"]);
$user = rtrim($_POST["user"]);
$password = rtrim($_POST["password"]);
require_once "src/ldap.php";
$result = LdapIsConnected($domain, $user, $password, []);
if (!$result) {
require_once "templates/login_failed.html";
exit;
}
require_once "view/View.php";
$info = LdapGetUserInfo($user);
$body = PrintLoginInfo($info);
require_once "templates/login_success.html.php";

36
js/datatables.min.js vendored Normal file

File diff suppressed because one or more lines are too long

245
lang/fr.json Normal file
View File

@@ -0,0 +1,245 @@
{
"emptyTable": "Aucune donnée disponible dans le tableau",
"loadingRecords": "Chargement...",
"processing": "Traitement...",
"select": {
"rows": {
"_": "%d lignes sélectionnées",
"1": "1 ligne sélectionnée"
},
"cells": {
"1": "1 cellule sélectionnée",
"_": "%d cellules sélectionnées"
},
"columns": {
"1": "1 colonne sélectionnée",
"_": "%d colonnes sélectionnées"
}
},
"autoFill": {
"cancel": "Annuler",
"fill": "Remplir toutes les cellules avec <i>%d<\/i>",
"fillHorizontal": "Remplir les cellules horizontalement",
"fillVertical": "Remplir les cellules verticalement"
},
"searchBuilder": {
"conditions": {
"date": {
"after": "Après le",
"before": "Avant le",
"between": "Entre",
"empty": "Vide",
"not": "Différent de",
"notBetween": "Pas entre",
"notEmpty": "Non vide",
"equals": "Égal à"
},
"number": {
"between": "Entre",
"empty": "Vide",
"gt": "Supérieur à",
"gte": "Supérieur ou égal à",
"lt": "Inférieur à",
"lte": "Inférieur ou égal à",
"not": "Différent de",
"notBetween": "Pas entre",
"notEmpty": "Non vide",
"equals": "Égal à"
},
"string": {
"contains": "Contient",
"empty": "Vide",
"endsWith": "Se termine par",
"not": "Différent de",
"notEmpty": "Non vide",
"startsWith": "Commence par",
"equals": "Égal à",
"notContains": "Ne contient pas",
"notEndsWith": "Ne termine pas par",
"notStartsWith": "Ne commence pas par"
},
"array": {
"empty": "Vide",
"contains": "Contient",
"not": "Différent de",
"notEmpty": "Non vide",
"without": "Sans",
"equals": "Égal à"
}
},
"add": "Ajouter une condition",
"button": {
"0": "Recherche avancée",
"_": "Recherche avancée (%d)"
},
"clearAll": "Effacer tout",
"condition": "Condition",
"data": "Donnée",
"deleteTitle": "Supprimer la règle de filtrage",
"logicAnd": "Et",
"logicOr": "Ou",
"title": {
"0": "Recherche avancée",
"_": "Recherche avancée (%d)"
},
"value": "Valeur",
"leftTitle": "Désindenter le critère",
"rightTitle": "Indenter le critère"
},
"searchPanes": {
"clearMessage": "Effacer tout",
"count": "{total}",
"title": "Filtres actifs - %d",
"collapse": {
"0": "Volet de recherche",
"_": "Volet de recherche (%d)"
},
"countFiltered": "{shown} ({total})",
"emptyPanes": "Pas de volet de recherche",
"loadMessage": "Chargement du volet de recherche...",
"collapseMessage": "Réduire tout",
"showMessage": "Montrer tout"
},
"buttons": {
"collection": "Collection",
"colvis": "Visibilité colonnes",
"colvisRestore": "Rétablir visibilité",
"copy": "Copier",
"copySuccess": {
"1": "1 ligne copiée dans le presse-papier",
"_": "%d lignes copiées dans le presse-papier"
},
"copyTitle": "Copier dans le presse-papier",
"csv": "CSV",
"excel": "Excel",
"pageLength": {
"-1": "Afficher toutes les lignes",
"_": "Afficher %d lignes",
"1": "Afficher 1 ligne"
},
"pdf": "PDF",
"print": "Imprimer",
"copyKeys": "Appuyez sur ctrl ou u2318 + C pour copier les données du tableau dans votre presse-papier.",
"createState": "Créer un état",
"removeAllStates": "Supprimer tous les états",
"removeState": "Supprimer",
"renameState": "Renommer",
"savedStates": "États sauvegardés",
"stateRestore": "État %d",
"updateState": "Mettre à jour"
},
"decimal": ",",
"datetime": {
"previous": "Précédent",
"next": "Suivant",
"hours": "Heures",
"minutes": "Minutes",
"seconds": "Secondes",
"unknown": "-",
"amPm": [
"am",
"pm"
],
"months": {
"0": "Janvier",
"2": "Mars",
"3": "Avril",
"4": "Mai",
"5": "Juin",
"6": "Juillet",
"8": "Septembre",
"9": "Octobre",
"10": "Novembre",
"1": "Février",
"11": "Décembre",
"7": "Août"
},
"weekdays": [
"Dim",
"Lun",
"Mar",
"Mer",
"Jeu",
"Ven",
"Sam"
]
},
"editor": {
"close": "Fermer",
"create": {
"title": "Créer une nouvelle entrée",
"button": "Nouveau",
"submit": "Créer"
},
"edit": {
"button": "Editer",
"title": "Editer Entrée",
"submit": "Mettre à jour"
},
"remove": {
"button": "Supprimer",
"title": "Supprimer",
"submit": "Supprimer",
"confirm": {
"_": "Êtes-vous sûr de vouloir supprimer %d lignes ?",
"1": "Êtes-vous sûr de vouloir supprimer 1 ligne ?"
}
},
"multi": {
"title": "Valeurs multiples",
"info": "Les éléments sélectionnés contiennent différentes valeurs pour cette entrée. Pour modifier et définir tous les éléments de cette entrée à la même valeur, cliquez ou tapez ici, sinon ils conserveront leurs valeurs individuelles.",
"restore": "Annuler les modifications",
"noMulti": "Ce champ peut être modifié individuellement, mais ne fait pas partie d'un groupe. "
},
"error": {
"system": "Une erreur système s'est produite (<a target=\"\\\" rel=\"nofollow\" href=\"\\\">Plus d'information<\/a>)."
}
},
"stateRestore": {
"removeSubmit": "Supprimer",
"creationModal": {
"button": "Créer",
"order": "Tri",
"paging": "Pagination",
"scroller": "Position du défilement",
"search": "Recherche",
"select": "Sélection",
"columns": {
"search": "Recherche par colonne",
"visible": "Visibilité des colonnes"
},
"name": "Nom :",
"searchBuilder": "Recherche avancée",
"title": "Créer un nouvel état",
"toggleLabel": "Inclus :"
},
"renameButton": "Renommer",
"duplicateError": "Il existe déjà un état avec ce nom.",
"emptyError": "Le nom ne peut pas être vide.",
"emptyStates": "Aucun état sauvegardé",
"removeConfirm": "Voulez vous vraiment supprimer %s ?",
"removeError": "Échec de la suppression de l'état.",
"removeJoiner": "et",
"removeTitle": "Supprimer l'état",
"renameLabel": "Nouveau nom pour %s :",
"renameTitle": "Renommer l'état"
},
"info": "Affichage de _START_ à _END_ sur _TOTAL_ entrées",
"infoEmpty": "Affichage de 0 à 0 sur 0 entrées",
"infoFiltered": "(filtrées depuis un total de _MAX_ entrées)",
"lengthMenu": "Afficher _MENU_ entrées",
"paginate": {
"first": "Première",
"last": "Dernière",
"next": "Suivante",
"previous": "Précédente"
},
"zeroRecords": "Aucune entrée correspondante trouvée",
"aria": {
"sortAscending": " : activer pour trier la colonne par ordre croissant",
"sortDescending": " : activer pour trier la colonne par ordre décroissant"
},
"infoThousands": " ",
"search": "Rechercher :",
"thousands": " "
}

View File

@@ -1,33 +0,0 @@
<?php
if (!isset($_POST["domain"]) || !isset($_POST["user"]) || !isset($_POST["password"])) {
require_once "templates/login_form.html";
exit;
}
$domain = rtrim($_POST["domain"]);
$user = rtrim($_POST["user"]);
$password = rtrim($_POST["password"]);
require_once "ldap.php";
$result = LdapConnect($domain, $user, $password, []);
ldap_parse_result($handle, $result, $error_code, $matched_dn, $error_message, $referrals, $controls);
if ($error_code != 0) {
require_once "templates/login_failed.html";
exit;
}
require_once "view/View.php";
$info = LdapGetUserInfo($user);
$body = PrintLoginInfo($info);
require_once "templates/login_success.html.php";
// TODO: style
// TODO: Mettre les tentatives dans la db

BIN
screenshots/admin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

BIN
screenshots/auth.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
screenshots/error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
screenshots/success.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

83
src/database.php Normal file
View File

@@ -0,0 +1,83 @@
<?php
$table_name = "authentification_attempts";
/**
* Connexion à la base de données
*/
function GetDbConnection(): ?PDO
{
$host = 'localhost';
$dbname = 'ldap';
$username = 'root';
$password = '4321';
try {
// Création d'une instance PDO pour la connexion à la base de données
$pdo = new PDO("mysql:host=$host;dbname=$dbname", $username, $password);
// Configuration du mode d'erreur de PDO pour les exceptions
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
return $pdo;
} catch (PDOException $e) {
echo "Échec de la connexion : " . $e->getMessage();
}
return null;
}
class AuthAttempt
{
public string $username;
public string $status;
public string $timestamp;
public string $ip_address;
public function __construct(
string $username,
string $status,
string $ip_address,
string $timestamp = "",
) {
$this->username = $username;
$this->status = $status;
$this->ip_address = $ip_address;
$this->timestamp = $timestamp;
}
}
/**
* Ajoute une entrée dans la table des tentatives de connexion
*/
function InsertLine(AuthAttempt $attempt)
{
global $table_name;
$pdo = GetDbConnection();
$query = $pdo->prepare("INSERT INTO $table_name(`username`, `status`, `ip_address`) VALUES (:user, :status, :ip);");
$query->bindValue(":user", $attempt->username, PDO::PARAM_STR);
$query->bindValue(":status", $attempt->status, PDO::PARAM_STR);
$query->bindValue(":ip", $attempt->ip_address, PDO::PARAM_STR);
$query->execute();
}
/**
* Récupère toutes les lignes de tentative de connexion
*/
function GetLines() : array {
global $table_name;
$pdo = GetDbConnection();
$query = $pdo->prepare("SELECT * FROM $table_name;");
$query->execute();
$lines = $query->fetchAll(\PDO::FETCH_ASSOC);
$result = [];
foreach($lines as $line) {
array_push($result, new AuthAttempt($line["username"], $line["status"], $line["ip_address"], $line["timestamp"]));
}
return $result;
}

View File

@@ -1,8 +1,13 @@
<?php
// Cette ligne permet d'autoriser les certificats auto-signés
putenv('LDAPTLS_REQCERT=never');
require_once "database.php";
$ldap_domain_name = "woodywood";
$handle = ldap_connect("ldap://$ldap_domain_name.local");
$handle = ldap_connect("ldaps://$ldap_domain_name.local");
ldap_set_option($handle, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($handle, LDAP_OPT_REFERRALS, 0);
@@ -18,26 +23,38 @@ class UserInfo
}
}
function LdapConnect(string $domain, string $username, string $password, ?array $controls): LDAP\Result|false
/**
* Connexion avec des identifiants
*/
function LdapConnect(string $domain, string $username, string $password): LDAP\Result|false
{
global $handle;
$bind = ldap_bind_ext($handle, $username . '@' . $domain, $password, $controls);
LogConnection();
$bind = ldap_bind_ext($handle, $username . '@' . $domain, $password);
return $bind;
}
function LogConnection() {}
function LdapConnectAndBind()
{
global $ldap_domain_name;
$ldap_instance = ldap_connect("ldap://$ldap_domain_name.local");
ldap_set_option($ldap_instance, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap_instance, LDAP_OPT_REFERRALS, 0);
ldap_bind($ldap_instance, "Administrateur@woodywood", "3AFISE+25");
return $ldap_instance;
/**
* Se connecte à l'AD et vérifie la validité des identifiants
*/
function LdapIsConnected(string $domain, string $username, string $password) : bool {
global $handle;
$result = LdapConnect($domain, $username, $password);
ldap_parse_result($handle, $result, $error_code, $matched_dn, $error_message, $referrals, $controls);
$success = $error_code == 0;
LogConnection($username, $success);
return $success;
}
/**
* Enregistre la tentative de connexion dans la base de données
*/
function LogConnection(string $username, bool $success) {
InsertLine(new AuthAttempt($username, $success ? "success" : "failure", $_SERVER['REMOTE_ADDR']));
}
/**
* Retourne les informations d'un utilisateur
*/
function LdapGetUserInfo(string $user): ?UserInfo
{
global $handle;
@@ -56,12 +73,18 @@ function LdapGetUserInfo(string $user): ?UserInfo
return null;
}
/**
* Permet de retourner les valeur dans une chaîne de caractère de type dn
*/
function GetValue(string $dnStr, string $key): array
{
preg_match_all("/$key=([^,]+)/", $dnStr, $matches);
return isset($matches[1]) ? $matches[1] : [];
}
/**
* Retourne tous les objets du type spécifié dans l'ou souhaitée et filtre le résultat
*/
function LdapGetObjectsInOU(string $ou, string $objectType, string $field): array
{
global $ldap_domain_name;
@@ -95,11 +118,17 @@ function LdapGetObjectsInOU(string $ou, string $objectType, string $field): arra
return $object_values;
}
/**
* Retourne la liste des utilisateurs d'une OU
*/
function LdapGetUsersInOU(string $ou): array
{
return LdapGetObjectsInOU($ou, "user", "CN");
}
/**
* Retourne la liste des groupes d'une OU
*/
function LdapGetGroupsInOU(string $ou): array
{
return LdapGetObjectsInOU($ou, "group", "CN");

View File

@@ -6,6 +6,8 @@
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="../css/index.css">
<link rel="stylesheet" href="../css/main.css">
<!-- Datatables CSS -->
<link rel="stylesheet" href="../css/dataTables.css">
</head>
<body>
<?php

View File

@@ -1,5 +1,10 @@
<?php
$admin_account = "Administrateur";
/**
* Affiche les éléments d'une liste
*/
function PrintListFirsts(string $title, array $liste): string
{
$result = '<div class="list-group-item"><h5>' . $title . '</h5>';
@@ -11,9 +16,16 @@ function PrintListFirsts(string $title, array $liste): string
return $result;
}
/**
* Affiche les informations de l'utilisateur
*/
function PrintLoginInfo($info)
{
global $admin_account;
$body = '<h2 class="text-center pt-3">Bienvenue ' . $info->fullName . " !</h2>";
if ($info->fullName == $admin_account) {
return $body .= PrintAdminInterface();
}
$body .= '<div class="list-group ">';
foreach ($info->ous as $ou) {
$body .= '<div class="list-group-item"><h3>' . $ou . "</h3>";
@@ -22,8 +34,54 @@ function PrintLoginInfo($info)
$body .= PrintListFirsts("Groupes", LdapGetGroupsInOU($ou));
$body .= "</div>";
$body .= "</div>";
}
$body .= "</div>";
return $body;
}
/**
* Retourne la chaîne traduite de l'état d'une tentative de connexion
*/
function translateSuccess(string $success) {
return $success == "success" ? "Succès" : "Échec";
}
/**
* Affiche l'interface administrateur
*/
function PrintAdminInterface(): string
{
$auth_attempts = GetLines();
$body = '<h5 class="text-center">Historique des connexions</h5>';
$body .= '<table id="attempts" class="table table-striped table-bordered">
<thead class="thead-light">
<tr>
<th scope="col">Utilisateur</th>
<th scope="col">Status</th>
<th scope="col">Date</th>
<th scope="col">Adresse IP</th>
</tr>
</thead>';
foreach ($auth_attempts as $attempt) {
$body .= '<tr>';
$body .= '<td>' . $attempt->username . '</td>';
$body .= '<td>' . translateSuccess($attempt->status) . '</td>';
$body .= '<td>' . $attempt->timestamp . '</td>';
$body .= '<td>' . $attempt->ip_address . '</td>';
$body .= '</tr>';
}
$body .= "</table>";
$body .= '
<script src="/js/datatables.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
let table = new DataTable("#attempts", {
language: {
url: "/lang/fr.json",
},
});
});
</script>
';
return $body;
}