feat: Audit trail pour actions sensibles
Story 1.7 - Implémente un système complet d'audit trail pour tracer toutes les actions sensibles (authentification, modifications de données, exports) avec immuabilité garantie par PostgreSQL. Fonctionnalités principales: - Table audit_log append-only avec contraintes PostgreSQL (RULE) - AuditLogger centralisé avec injection automatique du contexte - Correlation ID pour traçabilité distribuée (HTTP + async) - Handlers pour événements d'authentification - Commande d'archivage des logs anciens - Pas de PII dans les logs (emails/IPs hashés) Infrastructure: - Middlewares Messenger pour propagation du Correlation ID - HTTP middleware pour génération/propagation du Correlation ID - Support multi-tenant avec TenantResolver
This commit is contained in:
@@ -7,8 +7,10 @@ namespace App\Administration\Infrastructure\Security;
|
||||
use App\Administration\Domain\Event\CompteBloqueTemporairement;
|
||||
use App\Administration\Domain\Event\ConnexionEchouee;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use App\Shared\Infrastructure\RateLimit\LoginRateLimiterInterface;
|
||||
use App\Shared\Infrastructure\RateLimit\LoginRateLimitResult;
|
||||
use App\Shared\Infrastructure\Tenant\TenantResolver;
|
||||
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
@@ -20,12 +22,16 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Handles login failures: Fibonacci rate limiting, audit, user-friendly messages.
|
||||
*
|
||||
* Important: Never reveal whether the email exists or not (AC2).
|
||||
*
|
||||
* Note: /api/login is excluded from TenantMiddleware, so we resolve tenant
|
||||
* directly from host header using TenantResolver.
|
||||
*
|
||||
* @see Story 1.4 - T5: Backend Login Endpoint
|
||||
*/
|
||||
final readonly class LoginFailureHandler implements AuthenticationFailureHandlerInterface
|
||||
@@ -34,6 +40,7 @@ final readonly class LoginFailureHandler implements AuthenticationFailureHandler
|
||||
private LoginRateLimiterInterface $rateLimiter,
|
||||
private MessageBusInterface $eventBus,
|
||||
private Clock $clock,
|
||||
private TenantResolver $tenantResolver,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -49,9 +56,12 @@ final readonly class LoginFailureHandler implements AuthenticationFailureHandler
|
||||
// Record the failure and get the new state
|
||||
$result = $this->rateLimiter->recordFailure($request, $email);
|
||||
|
||||
// Dispatch the failure event
|
||||
// Resolve tenant from host header (TenantMiddleware skips /api/login)
|
||||
$tenantId = $this->resolveTenantFromHost($request);
|
||||
|
||||
$this->eventBus->dispatch(new ConnexionEchouee(
|
||||
email: $email,
|
||||
tenantId: $tenantId,
|
||||
ipAddress: $ipAddress,
|
||||
userAgent: $userAgent,
|
||||
reason: 'invalid_credentials',
|
||||
@@ -62,6 +72,7 @@ final readonly class LoginFailureHandler implements AuthenticationFailureHandler
|
||||
if ($result->ipBlocked) {
|
||||
$this->eventBus->dispatch(new CompteBloqueTemporairement(
|
||||
email: $email,
|
||||
tenantId: $tenantId,
|
||||
ipAddress: $ipAddress,
|
||||
userAgent: $userAgent,
|
||||
blockedForSeconds: $result->retryAfter ?? LoginRateLimiterInterface::IP_BLOCK_DURATION,
|
||||
@@ -125,4 +136,26 @@ final readonly class LoginFailureHandler implements AuthenticationFailureHandler
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve tenant from request host header.
|
||||
*
|
||||
* Since /api/login is excluded from TenantMiddleware, we must resolve
|
||||
* the tenant ourselves to properly scope audit events.
|
||||
*
|
||||
* Returns null if tenant cannot be resolved (unknown domain, database issues, etc.)
|
||||
* to ensure login failure handling never breaks due to tenant resolution.
|
||||
*/
|
||||
private function resolveTenantFromHost(Request $request): ?TenantId
|
||||
{
|
||||
try {
|
||||
$config = $this->tenantResolver->resolve($request->getHost());
|
||||
|
||||
return $config->tenantId;
|
||||
} catch (Throwable) {
|
||||
// Login attempt on unknown domain or tenant resolution failed
|
||||
// Don't let tenant resolution break the login failure handling
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user