Merge pull request #9 from tips-of-mine/feat/certificate-download

Implémenter la fonctionnalité de téléchargement des certificats depui…
This commit is contained in:
tips-of-mine
2025-06-15 14:40:16 +02:00
committed by GitHub
4 changed files with 256 additions and 1 deletions

View File

@ -186,6 +186,7 @@ $router->addRoute('GET', '/certificates', 'CertificateController@index', true);
$router->addRoute('GET', '/certificates/create', 'CertificateController@showCreateForm', true);
$router->addRoute('POST', '/certificates/create', 'CertificateController@create', true);
$router->addRoute('POST', '/certificates/revoke', 'CertificateController@revoke', true);
$router->addRoute('GET', '/certificates/download', 'CertificateController@download', true);
$router->addRoute('GET', '/perimeters', 'PerimeterController@index', true);
$router->addRoute('GET', '/perimeters/create', 'PerimeterController@showCreateForm', true);
$router->addRoute('POST', '/perimeters/create', 'PerimeterController@create', true);

View File

@ -248,4 +248,118 @@ class CertificateController
header('Location: /certificates');
exit();
}
/**
* Gère le téléchargement des fichiers de certificats et clés.
*/
public function download()
{
if (!$this->authService->isLoggedIn()) {
header('Location: /login');
exit();
}
// Récupérer les paramètres de la requête
$type = $_GET['type'] ?? null;
$fileName = $_GET['file'] ?? null;
$perimeterName = $_GET['perimeter'] ?? null; // Utilisé pour intermédiaires et simples
// Log de la tentative de téléchargement
$userId = $this->authService->getUserId();
$ipAddress = $_SERVER['REMOTE_ADDR'];
$this->logService->log('info', "Download attempt: type='{$type}', file='{$fileName}', perimeter='{$perimeterName}'", $userId, $ipAddress);
// Validation basique des paramètres
if (empty($type) || empty($fileName)) {
$_SESSION['error'] = 'Missing download parameters.';
$this->logService->log('warn', "Missing download parameters.", $userId, $ipAddress);
header('Location: /dashboard');
exit();
}
// Sécurisation des noms de fichier et de périmètre
if (basename($fileName) !== $fileName || ($perimeterName && basename($perimeterName) !== $perimeterName)) {
$_SESSION['error'] = 'Invalid characters in file or perimeter name.';
$this->logService->log('error', "Invalid characters in file or perimeter name for download.", $userId, $ipAddress);
header('Location: /dashboard');
exit();
}
$filePath = '';
switch ($type) {
case 'root':
if ($fileName === 'ca.cert.pem') {
$filePath = ROOT_CA_PATH . '/certs/' . $fileName;
} elseif ($fileName === 'ca.key.pem') {
if ($this->authService->getUserRole() === 'admin') {
$filePath = ROOT_CA_PATH . '/private/' . $fileName;
} else {
$_SESSION['error'] = 'Unauthorized to download root key.';
$this->logService->log('error', "Unauthorized attempt to download root CA key by user ID: {$userId}", $userId, $ipAddress);
header('Location: /dashboard');
exit();
}
} else {
$_SESSION['error'] = 'Invalid file specified for root certificate type.';
$this->logService->log('warn', "Invalid file '{$fileName}' specified for root certificate type.", $userId, $ipAddress);
header('Location: /dashboard');
exit();
}
break;
case 'intermediate':
if (empty($perimeterName)) {
$_SESSION['error'] = 'Perimeter name missing for intermediate certificate.';
$this->logService->log('warn', "Perimeter name missing for intermediate certificate download.", $userId, $ipAddress);
header('Location: /dashboard');
exit();
}
$filePath = INTERMEDIATE_CA_PATH_BASE . '/' . $perimeterName . '/certs/' . $fileName;
break;
case 'simple':
if (empty($perimeterName)) {
$_SESSION['error'] = 'Perimeter name missing for simple certificate.';
$this->logService->log('warn', "Perimeter name missing for simple certificate download.", $userId, $ipAddress);
header('Location: /dashboard');
exit();
}
$filePath = INTERMEDIATE_CA_PATH_BASE . '/' . $perimeterName . '/certs/' . $fileName;
break;
default:
$_SESSION['error'] = 'Invalid certificate type for download.';
$this->logService->log('warn', "Invalid certificate type for download: {$type}", $userId, $ipAddress);
header('Location: /dashboard');
exit();
}
if (!empty($filePath) && file_exists($filePath) && is_readable($filePath)) {
$mimeType = 'application/octet-stream';
$fileExtension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
if ($fileExtension === 'pem' || $fileExtension === 'key' || $fileExtension === 'crt' || $fileExtension === 'cer') {
$mimeType = 'application/x-pem-file';
}
header('Content-Description: File Transfer');
header('Content-Type: ' . $mimeType);
header('Content-Disposition: attachment; filename="' . basename($filePath) . '"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($filePath));
if (ob_get_level()) {
ob_end_clean();
}
readfile($filePath);
$this->logService->log('info', "File '{$filePath}' downloaded successfully.", $userId, $ipAddress);
exit;
} else {
$_SESSION['error'] = 'File not found or not readable: ' . htmlspecialchars($fileName);
$this->logService->log('error', "File not found or not readable for download: {$filePath} (Type: {$type}, File: {$fileName}, Perimeter: {$perimeterName})", $userId, $ipAddress);
header('Location: /dashboard');
exit();
}
}
}

View File

@ -2,10 +2,11 @@
namespace App\Controllers;
use App\Core\Database;
use App\Services\AuthService;
use App\Services\LanguageService;
use App\Utils\DarkMode;
// Ensure App\Core\Database is imported
use App\Core\Database;
/**
* Contrôleur pour la page du tableau de bord.
@ -14,14 +15,19 @@ class DashboardController
{
private $authService;
private $langService;
private $db; // Property to hold the database instance
/**
* Constructeur du DashboardController.
*/
public function __construct()
{
// Initialize database connection here if it's meant to be a class property
// For now, authService initializes its own, and index() gets a new instance.
// If $this->db was intended, it should be $this->db = Database::getInstance();
$this->authService = new AuthService(Database::getInstance());
$this->langService = new LanguageService(APP_ROOT_DIR . '/src/Lang/');
// $this->db = Database::getInstance(); // Uncomment if $db should be a class property accessible in index() via $this->db
}
/**
@ -42,6 +48,78 @@ class DashboardController
$darkModeClass = DarkMode::getBodyClass();
$userRole = $this->authService->getUserRole(); // Pour afficher/masquer certains éléments
// Initialize database connection
// If $this->db was initialized in constructor, use $db = $this->db;
$db = Database::getInstance(); // Using a local instance as per original structure
// Initialize structured certificates array
$structuredCertificates = [
'root' => null,
'intermediates' => [],
];
// Fetch Root Certificate
$stmt = $db->prepare("SELECT name FROM certificates WHERE type = 'root' LIMIT 1");
$stmt->execute();
$rootCert = $stmt->fetch();
if ($rootCert) {
$structuredCertificates['root'] = [
'name' => $rootCert['name'],
'cert_path' => ROOT_CA_PATH . '/certs/ca.cert.pem',
'key_path' => ROOT_CA_PATH . '/private/ca.key.pem', // Corrected path as per instructions
];
} else {
// Handle case where root certificate is not found, though unlikely
// You might want to log this or set a default structure
$structuredCertificates['root'] = [
'name' => 'N/A',
'cert_path' => null,
'key_path' => null,
];
}
// Fetch Intermediate Certificates
$stmt = $db->prepare("
SELECT c.id, c.name, c.functional_perimeter_id, fp.name as perimeter_name
FROM certificates c
JOIN functional_perimeters fp ON c.functional_perimeter_id = fp.id
WHERE c.type = 'intermediate'
ORDER BY fp.name ASC, c.name ASC
");
$stmt->execute();
$intermediateCerts = $stmt->fetchAll();
foreach ($intermediateCerts as $interCert) {
$intermediateData = [
'id' => $interCert['id'],
'name' => $interCert['name'],
'perimeter_name' => $interCert['perimeter_name'],
'functional_perimeter_id' => $interCert['functional_perimeter_id'], // Pass perimeter_id for linking
'final_certificates' => [],
];
// Fetch Final Certificates for this Intermediate
// Ensure functional_perimeter_id is used in the query for 'simple' certificates
$stmtFinal = $db->prepare("
SELECT name, type, expiration_date, is_revoked
FROM certificates
WHERE type = 'simple' AND functional_perimeter_id = ?
ORDER BY name ASC
");
// Use $interCert['functional_perimeter_id'] which is the ID of the perimeter for this intermediate cert
$stmtFinal->execute([$interCert['functional_perimeter_id']]);
$finalCerts = $stmtFinal->fetchAll();
if ($finalCerts) {
foreach ($finalCerts as $finalCert) {
$intermediateData['final_certificates'][] = $finalCert;
}
}
$structuredCertificates['intermediates'][] = $intermediateData;
}
// Pass data to the view
require_once APP_ROOT_DIR . '/src/Views/dashboard/index.php';
}
}

View File

@ -27,6 +27,68 @@ require_once APP_ROOT_DIR . '/src/Views/shared/header.php';
<?php endif; ?>
</ul>
</section>
<section class="certificates-overview">
<h2><?= htmlspecialchars($translations['certificates_overview_title'] ?? 'Certificates Overview') ?></h2>
<!-- Root Certificate -->
<h3><?= htmlspecialchars($translations['root_certificate_title'] ?? 'Root Certificate') ?></h3>
<?php if (isset($structuredCertificates['root']) && $structuredCertificates['root']): ?>
<div>
<p><strong><?= htmlspecialchars($translations['name'] ?? 'Name:') ?></strong> <?= htmlspecialchars($structuredCertificates['root']['name']) ?></p>
<p>
<a href="/certificates/download?type=root&file=ca.cert.pem" class="button">
<?= htmlspecialchars($translations['download_certificate_pem'] ?? 'Download Certificate (.pem)') ?>
</a>
</p>
<p>
<a href="/certificates/download?type=root&file=ca.key.pem" class="button">
<?= htmlspecialchars($translations['download_key_pem'] ?? 'Download Private Key (.key)') ?>
</a>
</p>
</div>
<?php else: ?>
<p><?= htmlspecialchars($translations['root_certificate_not_configured'] ?? 'Root certificate is not configured.') ?></p>
<?php endif; ?>
<!-- Intermediate Certificates -->
<h3><?= htmlspecialchars($translations['intermediate_certificates_title'] ?? 'Intermediate Certificates') ?></h3>
<?php if (isset($structuredCertificates['intermediates']) && !empty($structuredCertificates['intermediates'])): ?>
<?php foreach ($structuredCertificates['intermediates'] as $intermediate): ?>
<div class="intermediate-certificate">
<h4><?= htmlspecialchars($intermediate['name']) ?> (<?= htmlspecialchars($translations['perimeter'] ?? 'Perimeter:') ?> <?= htmlspecialchars($intermediate['perimeter_name']) ?>)</h4>
<p>
<a href="/certificates/download?type=intermediate&perimeter=<?= urlencode($intermediate['perimeter_name']) ?>&file=intermediate.cert.pem" class="button">
<?= htmlspecialchars($translations['download_certificate_pem'] ?? 'Download Certificate (.pem)') ?>
</a>
</p>
<h5><?= htmlspecialchars($translations['associated_final_certificates_title'] ?? 'Associated Final Certificates') ?></h5>
<?php if (isset($intermediate['final_certificates']) && !empty($intermediate['final_certificates'])): ?>
<ul>
<?php foreach ($intermediate['final_certificates'] as $finalCert): ?>
<li>
<?= htmlspecialchars($finalCert['name']) ?>
(<?= htmlspecialchars($translations['type'] ?? 'Type:') ?> <?= htmlspecialchars($finalCert['type']) ?>,
<?= htmlspecialchars($translations['expires'] ?? 'Expires:') ?> <?= htmlspecialchars($finalCert['expiration_date']) ?>,
<?= htmlspecialchars($translations['revoked'] ?? 'Revoked:') ?> <?= $finalCert['is_revoked'] ? htmlspecialchars($translations['yes'] ?? 'Yes') : htmlspecialchars($translations['no'] ?? 'No') ?>)
<p>
<a href="/certificates/download?type=simple&perimeter=<?= urlencode($intermediate['perimeter_name']) ?>&file=<?= urlencode($finalCert['name']) ?>" class="button download-button-small">
<?= htmlspecialchars($translations['download_certificate_pem'] ?? 'Download Certificate (.pem)') ?>
</a>
</p>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<p><?= htmlspecialchars($translations['no_associated_final_certificates'] ?? 'No final certificates associated with this intermediate.') ?></p>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php else: ?>
<p><?= htmlspecialchars($translations['no_intermediate_certificates_found'] ?? 'No intermediate certificates found.') ?></p>
<?php endif; ?>
</section>
</div>
<?php