```
✨ feat(security): Ajoute blocage intranet et vérification des services.
```
This commit is contained in:
@@ -9,28 +9,87 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Twig\Environment;
|
||||
|
||||
#[AsEventListener(RequestEvent::class,method: 'onLocked',priority: 10)]
|
||||
#[AsEventListener(RequestEvent::class,method: 'onControl',priority: 5)]
|
||||
#[AsEventListener(RequestEvent::class, method: 'onLocked', priority: 10)]
|
||||
#[AsEventListener(RequestEvent::class, method: 'onControl', priority: 5)]
|
||||
class IntranetLocked
|
||||
{
|
||||
public function __construct(private readonly Environment $environment,
|
||||
private readonly Client $signatureClient,
|
||||
private readonly \App\Service\Search\Client $searchClient,
|
||||
private readonly \App\Service\Stripe\Client $stripeClient,
|
||||
private readonly Mailer $mailer
|
||||
private const WHITELISTED_IPS = [
|
||||
'212.114.31.239', // Bureau
|
||||
'1.2.3.4', // Autre
|
||||
'172.19.0.1',
|
||||
];
|
||||
|
||||
){
|
||||
public function __construct(
|
||||
private readonly Environment $environment,
|
||||
private readonly Client $signatureClient,
|
||||
private readonly \App\Service\Search\Client $searchClient,
|
||||
private readonly \App\Service\Stripe\Client $stripeClient,
|
||||
private readonly Mailer $mailer
|
||||
) {}
|
||||
|
||||
}
|
||||
|
||||
public function onLocked(RequestEvent $requestEvent)
|
||||
public function onLocked(RequestEvent $requestEvent): void
|
||||
{
|
||||
if($_ENV['INTRANET_LOCK'] == "true" && ! $this->isDebugRoute($requestEvent)) {
|
||||
// Si l'IP est autorisée ou si c'est une route de debug, on ne bloque pas
|
||||
if ($this->isWhitelisted($requestEvent) || $this->isDebugRoute($requestEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (($_ENV['INTRANET_LOCK'] ?? 'false') === "true") {
|
||||
$response = new Response($this->environment->render('security/locked.twig'));
|
||||
$response->setStatusCode(Response::HTTP_FORBIDDEN);
|
||||
$requestEvent->setResponse($response);
|
||||
}
|
||||
}
|
||||
|
||||
public function onControl(RequestEvent $requestEvent): void
|
||||
{
|
||||
// On ignore également le contrôle des services pour l'IP whitelisted
|
||||
if ($this->isWhitelisted($requestEvent) || $this->isDebugRoute($requestEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$isValid = true;
|
||||
$message = [];
|
||||
|
||||
// Check Signature
|
||||
if (!$this->signatureClient->status()) {
|
||||
$isValid = false;
|
||||
$message = ['service' => 'Signature', 'status' => 'Hors Service'];
|
||||
$this->advertTech($message);
|
||||
}
|
||||
|
||||
// Check Recherche (seulement si le précédent est encore valide ou pour accumuler les erreurs)
|
||||
if ($isValid && !$this->searchClient->status()) {
|
||||
$isValid = false;
|
||||
$message = ['service' => 'Recherche', 'status' => 'Hors Service'];
|
||||
$this->advertTech($message);
|
||||
}
|
||||
|
||||
// Check Stripe
|
||||
if ($isValid && !$this->stripeClient->status()) {
|
||||
$isValid = false;
|
||||
$message = ['service' => 'Stripe', 'status' => 'Hors Service'];
|
||||
$this->advertTech($message);
|
||||
}
|
||||
|
||||
if (!$isValid) {
|
||||
$response = new Response($this->environment->render('security/error.twig', [
|
||||
'message' => $message,
|
||||
]));
|
||||
$response->setStatusCode(Response::HTTP_FORBIDDEN);
|
||||
$requestEvent->setResponse($response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'IP du client est l'IP de maintenance
|
||||
*/
|
||||
private function isWhitelisted(RequestEvent $event): bool
|
||||
{
|
||||
$request = $event->getRequest();
|
||||
$clientIp = $request->headers->get('cf-connecting-ip') ?? $request->getClientIp();
|
||||
|
||||
return in_array($clientIp, self::WHITELISTED_IPS, true);
|
||||
}
|
||||
|
||||
private function isDebugRoute(RequestEvent $event): bool
|
||||
@@ -38,52 +97,15 @@ class IntranetLocked
|
||||
$path = $event->getRequest()->getPathInfo();
|
||||
return str_contains($path, "_wdt") || str_contains($path, "_profiler");
|
||||
}
|
||||
public function onControl(RequestEvent $requestEvent)
|
||||
|
||||
public function advertTech(array $message): void
|
||||
{
|
||||
if ($this->isDebugRoute($requestEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$isValid = true;
|
||||
$message = [];
|
||||
if(!$this->signatureClient->status()) {
|
||||
$isValid = false;
|
||||
$message = [
|
||||
'service' => 'Signature',
|
||||
'status' => 'Hors Service'
|
||||
];
|
||||
$this->advertTech($message);
|
||||
}
|
||||
if(!$this->searchClient->status()) {
|
||||
$isValid = false;
|
||||
$message = [
|
||||
'service' => 'Recherche',
|
||||
'status' => 'Hors Service'
|
||||
];
|
||||
$this->advertTech($message);
|
||||
}
|
||||
if(!$this->stripeClient->status()) {
|
||||
$isValid = false;
|
||||
$message = [
|
||||
'service' => 'Stripe',
|
||||
'status' => 'Hors Service'
|
||||
];
|
||||
$this->advertTech($message);
|
||||
}
|
||||
if(!$isValid) {
|
||||
$response = new Response($this->environment->render('security/error.twig',[
|
||||
'message' => $message,
|
||||
]));
|
||||
$response->setStatusCode(Response::HTTP_FORBIDDEN);
|
||||
$requestEvent->setResponse($response);
|
||||
}
|
||||
}
|
||||
public function advertTech(array $message) {
|
||||
$this->mailer->send('notification@siteconseil.fr',"Notification siteconseil",
|
||||
"[Intranet Ludikevent] - Accées impossible suite à une erreur de service",
|
||||
"mails/tech/noaccess.twig",[
|
||||
'message' => $message
|
||||
]);
|
||||
$this->mailer->send(
|
||||
'notification@siteconseil.fr',
|
||||
"Notification siteconseil",
|
||||
"[Intranet Ludikevent] - Accès impossible suite à une erreur de service",
|
||||
"mails/tech/noaccess.twig",
|
||||
['message' => $message]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,83 +68,6 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{# ... après le div du footer ... #}
|
||||
<div class="mt-10 p-6 backdrop-blur-md bg-black/20 border border-white/5 rounded-3xl overflow-hidden">
|
||||
<h3 class="text-[10px] font-black text-blue-500 uppercase tracking-[0.2em] mb-4 flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
Request Headers (Debug)
|
||||
</h3>
|
||||
|
||||
<div class="mt-10 p-6 backdrop-blur-md bg-black/20 border border-white/5 rounded-3xl overflow-hidden">
|
||||
<h3 class="text-[10px] font-black text-blue-500 uppercase tracking-[0.2em] mb-4 flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
Réseau & Cloudflare (Debug)
|
||||
</h3>
|
||||
|
||||
{# Affichage de l'IP réelle calculée par Symfony #}
|
||||
<div class="mb-6 grid grid-cols-2 gap-4">
|
||||
<div class="p-3 bg-blue-600/10 rounded-xl border border-blue-500/20">
|
||||
<p class="text-[9px] text-slate-500 uppercase font-bold">Client IP (Symfony)</p>
|
||||
<p class="text-sm font-mono text-white">{{ app.request.clientIp }}</p>
|
||||
</div>
|
||||
<div class="p-3 bg-purple-600/10 rounded-xl border border-purple-500/20">
|
||||
<p class="text-[9px] text-slate-500 uppercase font-bold">Origine (CF-Country)</p>
|
||||
<p class="text-sm font-mono text-white">{{ app.request.headers.get('cf-ipcountry')|default('N/A') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b border-white/10">
|
||||
<th class="py-2 text-[9px] font-bold text-slate-500 uppercase tracking-wider">Header</th>
|
||||
<th class="py-2 text-[9px] font-bold text-slate-500 uppercase tracking-wider pl-4">Valeur</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-white/5">
|
||||
{# On filtre pour afficher les headers Cloudflare en priorité #}
|
||||
{% for key, values in app.request.headers.all %}
|
||||
{% if "cf-" in key or "x-forwarded" in key %}
|
||||
<tr class="group hover:bg-white/5 transition-colors">
|
||||
<td class="py-3 text-[11px] font-mono text-blue-400 align-top">
|
||||
<span class="bg-blue-500/10 px-1.5 py-0.5 rounded text-[9px]">{{ key }}</span>
|
||||
</td>
|
||||
<td class="py-3 pl-4 text-[11px] text-slate-300 align-top break-all">
|
||||
{{ values|join(', ') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b border-white/10">
|
||||
<th class="py-2 text-[9px] font-bold text-slate-500 uppercase tracking-wider">Clé</th>
|
||||
<th class="py-2 text-[9px] font-bold text-slate-500 uppercase tracking-wider pl-4">Valeur</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-white/5">
|
||||
{% for key, values in app.request.headers.all %}
|
||||
<tr class="group hover:bg-white/5 transition-colors">
|
||||
<td class="py-3 text-[11px] font-mono text-blue-400 align-top break-all">{{ key }}</td>
|
||||
<td class="py-3 pl-4 text-[11px] text-slate-300 align-top break-all">
|
||||
{# Les headers peuvent être des tableaux, on les joint proprement #}
|
||||
{{ values|join(', ') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user