Refactor for monolog in security controller

This commit is contained in:
Charles 2025-12-09 09:40:13 +01:00
parent 5f4336d824
commit 3f9d388f7f
6 changed files with 137 additions and 132 deletions

View File

@ -5,6 +5,7 @@ namespace App\Controller;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use App\Repository\UsersOrganizationsRepository; use App\Repository\UsersOrganizationsRepository;
use App\Service\AccessTokenService; use App\Service\AccessTokenService;
use App\Service\LoggerService;
use App\Service\OrganizationsService; use App\Service\OrganizationsService;
use App\Service\UserService; use App\Service\UserService;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -30,7 +31,7 @@ class SecurityController extends AbstractController
private readonly UsersOrganizationsRepository $uoRepository, private readonly UsersOrganizationsRepository $uoRepository,
private readonly LoggerInterface $logger, private readonly LoggerInterface $logger,
private readonly EntityManagerInterface $entityManager, private readonly EntityManagerInterface $entityManager,
private readonly OrganizationsService $organizationsService) private readonly OrganizationsService $organizationsService, private readonly LoggerService $loggerService, private readonly Security $security)
{ {
$this->cguUserService = $cguUserService; $this->cguUserService = $cguUserService;
} }
@ -50,14 +51,16 @@ class SecurityController extends AbstractController
#[Route(path: '/sso_logout', name: 'sso_logout')] #[Route(path: '/sso_logout', name: 'sso_logout')]
public function ssoLogout(RequestStack $stack, LoggerInterface $logger, AccessTokenService $accessTokenService, Security $security): Response public function ssoLogout(RequestStack $stack, LoggerInterface $logger, AccessTokenService $accessTokenService, Security $security): Response
{ {
try{ try {
if( $stack->getSession()->invalidate()){ $user = $this->userService->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
$accessTokenService->revokeTokens($security->getUser()->getUserIdentifier()); $id = $user->getId();
if ($stack->getSession()->invalidate()) {
$accessTokenService->revokeUserTokens($security->getUser()->getUserIdentifier());
$security->logout(false); $security->logout(false);
$logger->info("Logout successfully"); $this->loggerService->logUserConnection('User logged out', ['user_id' => $id]);
return $this->redirect('/'); return $this->redirect('/');
} }
}catch (\Exception $e){ } catch (\Exception $e) {
$logger->log(LogLevel::ERROR, 'Error invalidating session: ' . $e->getMessage()); $logger->log(LogLevel::ERROR, 'Error invalidating session: ' . $e->getMessage());
} }
return $this->redirectToRoute('app_index'); return $this->redirectToRoute('app_index');
@ -69,6 +72,7 @@ class SecurityController extends AbstractController
if ($request->isMethod('POST')) { if ($request->isMethod('POST')) {
if (!$request->request->has('decline')) { if (!$request->request->has('decline')) {
$this->cguUserService->acceptLatestCgu($this->getUser()); $this->cguUserService->acceptLatestCgu($this->getUser());
$this->loggerService->logCGUAcceptance($this->getUser()->getId());
} }
return $this->redirectToRoute('oauth2_authorize', $request->query->all()); return $this->redirectToRoute('oauth2_authorize', $request->query->all());
@ -83,12 +87,24 @@ class SecurityController extends AbstractController
$error = $request->get('error'); $error = $request->get('error');
$user = $this->userRepository->find($id); $user = $this->userRepository->find($id);
if (!$user) { if (!$user) {
$this->loggerService->logEntityNotFound('User', ['user_id' => $id,
'error' => $error ?? null,
'message' => 'user not found for password setup'], $id);
throw $this->createNotFoundException(self::NOT_FOUND); throw $this->createNotFoundException(self::NOT_FOUND);
} }
$token = $request->get('token'); $token = $request->get('token');
if (empty($token) || !$this->userService->isPasswordTokenValid($user, $token)) { if (empty($token)) {
$error = 'Le lien de définition du mot de passe est invalide ou a expiré. Veuillez en demander un nouveau.'; $error = 'Le lien de définition du mot de passe est invalide ou a expiré. Veuillez en demander un nouveau.';
$this->logger->warning($user->getUserIdentifier(). " tried to use an invalid or expired password setup token."); $this->loggerService->logTokenError('Token empty while trying to setup password', ['token' => $token,
'token_empty' => true,
'user_id' => $id,
'message' => 'empty token provided for password setup']);
}
if (!$this->userService->isPasswordTokenValid($user, $token)) {
$error = 'Le lien de définition du mot de passe est invalide ou a expiré. Veuillez en demander un nouveau.';
$this->loggerService->logTokenError('invalid or expired token for password setup', ['user_id' => $id,
'token' => $token,]);
} }
return $this->render('security/password_setup.html.twig', [ return $this->render('security/password_setup.html.twig', [
'id' => $id, 'id' => $id,
@ -102,34 +118,38 @@ class SecurityController extends AbstractController
{ {
$user = $this->userRepository->find($id); $user = $this->userRepository->find($id);
if (!$user) { if (!$user) {
$this->loggerService->logEntityNotFound('User', ['user_id' => $id,
'message' => 'user not found for password reset'], $id);
throw $this->createNotFoundException(self::NOT_FOUND); throw $this->createNotFoundException(self::NOT_FOUND);
} }
$newPassword = $_POST['_password'] ?? null; $newPassword = $_POST['_password'] ?? null;
$confirmPassword = $_POST['_passwordConfirm'] ?? null; $confirmPassword = $_POST['_passwordConfirm'] ?? null;
if ($newPassword !== $confirmPassword) { if ($newPassword !== $confirmPassword) {
$error = 'Les mots de passe ne correspondent pas. Veuillez réessayer.'; $error = 'Les mots de passe ne correspondent pas. Veuillez réessayer.';
$this->logger->warning($user->getUserIdentifier(). " provided non-matching passwords during password reset."); $this->loggerService->logUserAction($id, $id, 'Password confirmation does not match during password reset.');
return $this->redirectToRoute('password_setup', [ return $this->redirectToRoute('password_setup', [
'id' => $id, 'id' => $id,
'token' => $_POST['token'] ?? '', 'token' => $_POST['token'] ?? '',
'error'=> $error]); 'error' => $error]);
} }
if (!$this->userService->isPasswordStrong($newPassword)) { if (!$this->userService->isPasswordStrong($newPassword)) {
$error = 'Le mot de passe ne respecte pas les critères de sécurité. Veuillez en choisir un autre.'; $error = 'Le mot de passe ne respecte pas les critères de sécurité. Veuillez en choisir un autre.';
$this->logger->warning($user->getUserIdentifier(). " provided a weak password during password reset."); $this->loggerService->logUserAction($id, $id, ' provided a weak password during password reset.');
return $this->redirectToRoute('password_setup', ['id' => $id, 'token' => $_POST['token'] ?? '', 'error'=> $error]); return $this->redirectToRoute('password_setup', ['id' => $id, 'token' => $_POST['token'] ?? '', 'error' => $error]);
} }
$this->userService->updateUserPassword($user, $newPassword); $this->userService->updateUserPassword($user, $newPassword);
$orgId = $this->userService->getOrgFromToken( $_POST['token']); $this->loggerService->logUserAction($id, $id, 'Password reset user successfully.');
$orgId = $this->userService->getOrgFromToken($_POST['token']);
$uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]); $uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]);
if($uo){ if ($uo) {
$uo->setStatut("ACCEPTED"); $uo->setStatut("ACCEPTED");
$uo->setIsActive(true); $uo->setIsActive(true);
$this->entityManager->persist($uo); $this->entityManager->persist($uo);
$this->entityManager->flush(); $this->entityManager->flush();
$this->loggerService->logOrganizationInformation($orgId, $user->getId(), 'User accepted organization invitation during password reset.');
$this->loggerService->logUserAction($id, $id, "User accepted organization invitation successfully with uo link id : {$uo->getId()}");
$data = ['user' => $user, $data = ['user' => $user,
'organization' => $uo->getOrganization(), 'organization' => $uo->getOrganization(),
'ip' => $request->getClientIp(),
]; ];
$this->organizationsService->notifyOrganizationAdmins($data, "USER_ACCEPTED"); $this->organizationsService->notifyOrganizationAdmins($data, "USER_ACCEPTED");

View File

@ -13,6 +13,7 @@ use App\Repository\OrganizationsRepository;
use App\Repository\RolesRepository; use App\Repository\RolesRepository;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use App\Repository\UsersOrganizationsRepository; use App\Repository\UsersOrganizationsRepository;
use App\Service\AccessTokenService;
use App\Service\ActionService; use App\Service\ActionService;
use App\Service\AwsService; use App\Service\AwsService;
use App\Service\EmailService; use App\Service\EmailService;
@ -45,17 +46,17 @@ class UserController extends AbstractController
private readonly UserOrganizationService $userOrganizationService, private readonly UserOrganizationService $userOrganizationService,
private readonly UserRepository $userRepository, private readonly UserRepository $userRepository,
private readonly UsersOrganizationsRepository $uoRepository, private readonly UsersOrganizationsRepository $uoRepository,
private readonly OrganizationsRepository $organizationRepository, private readonly OrganizationsRepository $organizationRepository,
private readonly LoggerInterface $userManagementLogger, private readonly LoggerInterface $userManagementLogger,
private readonly LoggerInterface $organizationManagementLogger, private readonly LoggerInterface $organizationManagementLogger,
private readonly LoggerInterface $errorLogger, private readonly LoggerInterface $errorLogger,
private readonly LoggerInterface $securityLogger, private readonly LoggerInterface $securityLogger,
private readonly LoggerService $loggerService, private readonly LoggerService $loggerService,
private readonly EmailService $emailService, private readonly EmailService $emailService,
private readonly AwsService $awsService, private readonly AwsService $awsService,
private readonly OrganizationsService $organizationsService, private readonly OrganizationsService $organizationsService,
private readonly AppsRepository $appsRepository, private readonly AppsRepository $appsRepository,
private readonly RolesRepository $rolesRepository, private readonly RolesRepository $rolesRepository, private readonly AccessTokenService $accessTokenService,
) )
{ {
} }
@ -405,7 +406,7 @@ class UserController extends AbstractController
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user); $this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user);
if ($this->userService->isUserConnected($user->getUserIdentifier())) { if ($this->userService->isUserConnected($user->getUserIdentifier())) {
$this->userService->revokeUserTokens($user->getUserIdentifier()); $this->accessTokenService->revokeUserTokens($user->getUserIdentifier());
} }
$user->setModifiedAt(new \DateTimeImmutable('now')); $user->setModifiedAt(new \DateTimeImmutable('now'));
@ -563,7 +564,7 @@ class UserController extends AbstractController
// Revoke tokens if connected // Revoke tokens if connected
if ($this->userService->isUserConnected($user->getUserIdentifier())) { if ($this->userService->isUserConnected($user->getUserIdentifier())) {
$this->userService->revokeUserTokens($user->getUserIdentifier()); $this->accessTokenService->revokeUserTokens($user->getUserIdentifier());
} }
$this->entityManager->flush(); $this->entityManager->flush();

View File

@ -11,17 +11,38 @@ class AccessTokenService
private EntityManagerInterface $entityManager; private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager) public function __construct(EntityManagerInterface $entityManager,
private readonly LoggerService $loggerService)
{ {
$this->entityManager = $entityManager; $this->entityManager = $entityManager;
} }
public function revokeTokens(String $userIdentifier): void { public function revokeUserTokens(string $userIdentifier): void
$accessTokens = $this->entityManager->getRepository(AccessToken::class)->findBy(['userIdentifier' => $userIdentifier, 'revoked' => false]); {
foreach($accessTokens as $accessToken) { $tokens = $this->entityManager->getRepository(AccessToken::class)->findBy([
$accessToken->revoke(); 'userIdentifier' => $userIdentifier,
$this->entityManager->persist($accessToken); 'revoked' => false
$this->entityManager->flush(); ]);
foreach ($tokens as $token) {
try{
$token->revoke();
$this->loggerService->logTokenRevocation(
'Access token revoked for user',
[
'user_identifier' => $userIdentifier,
'token_id' => $token->getIdentifier(),
]
);
}catch (\Exception $e){
$this->loggerService->logError(
'Error revoking access token: ' . $e->getMessage(),
[
'user_identifier' => $userIdentifier,
'token_id' => $token->getIdentifier(),
]
);
}
} }
} }

View File

@ -78,16 +78,12 @@ readonly class LoggerService
]); ]);
} }
public function logAdminNotified(int $adminUserId, int $targetUserId, int $orgId, int $actingUserId): void public function logAdminNotified(array $array): void
{ {
$this->emailNotificationLogger->notice('Organization admin notified', [ $this->emailNotificationLogger->notice('Organization admin notified', array_merge($array, [
'admin_user_id' => $adminUserId,
'target_user_id' => $targetUserId,
'organization_id' => $orgId,
'acting_user_id' => $actingUserId,
'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
'timestamp' => $this->now(), 'timestamp' => $this->now(),
]); ]));
} }
public function logSuperAdmin(int $userId, ?int $orgId = null, int $actingUserId, string $message): void public function logSuperAdmin(int $userId, ?int $orgId = null, int $actingUserId, string $message): void
@ -224,4 +220,34 @@ readonly class LoggerService
'timestamp' => $this->now(), 'timestamp' => $this->now(),
]); ]);
} }
public function logUserConnection(string $message, array $array)
{
$this->securityLogger->info($message, array_merge($array, [
'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
'timestamp' => $this->now(),
]));
}
public function logCGUAcceptance(int $it)
{
$this->userManagementLogger->info("User accepted CGU", [
'user_id' => $it,
'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
'timestamp' => $this->now(),
]);
$this->securityLogger->info("User accepted CGU", [
'user_id' => $it,
'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
'timestamp' => $this->now(),
]);
}
public function logTokenError(string $message, array $context = []): void
{
$this->securityLogger->error($message, array_merge($context, [
'timestamp' => $this->now(),
'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
]));
}
} }

View File

@ -99,14 +99,11 @@ class OrganizationsService
$newUser, $newUser,
$data['organization'] $data['organization']
); );
$this->loggerService->logAdminNotified([
'admin_user_id' =>$adminUO->getUsers()->getId(),
'target_user_id' => $newUser->getId(),
'organization_id' => $data['organization']->getId(),'case' =>$type]);
} }
$this->emailNotificationLogger->info('Organization admins notified of new user accept', [
'admin_user_id' => $adminUO->getUsers()->getId(),
'organization_id' => $data['organization']->getId(),
'ip' => $data['ip'] ?? null,
'user_accepted' => $newUser->getId(),
'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM),
]);
break; break;
case 'USER_INVITED': case 'USER_INVITED':
if ($uoa) { if ($uoa) {
@ -116,14 +113,12 @@ class OrganizationsService
$invitedUser, $invitedUser,
$data['organization'] $data['organization']
); );
$this->loggerService->logAdminNotified([
'admin_user_id' =>$adminUO->getUsers()->getId(),
'target_user_id' => $newUser->getId(),
'organization_id' => $data['organization']->getId(),'case' =>$type]);
} }
$this->emailNotificationLogger->info('Organization admins notified of new user invited', [
'admin_user_id' => $adminUO->getUsers()->getId(),
'organization_id' => $data['organization']->getId(),
'ip' => $data['ip'] ?? null,
'user_accepted' => $invitedUser->getId(),
'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM),
]);
break; break;
case 'USER_DEACTIVATED': case 'USER_DEACTIVATED':
if ($uoa && $adminUO->getUsers()->getId() !== $data['user']->getId() ) { if ($uoa && $adminUO->getUsers()->getId() !== $data['user']->getId() ) {
@ -133,14 +128,12 @@ class OrganizationsService
$removedUser, $removedUser,
$data['organization'] $data['organization']
); );
$this->loggerService->logAdminNotified([
'admin_user_id' =>$adminUO->getUsers()->getId(),
'target_user_id' => $newUser->getId(),
'organization_id' => $data['organization']->getId(),'case' =>$type]);
} }
$this->emailNotificationLogger->info('Organization admins notified of user deactivated', [
'admin_user_id' => $adminUO->getUsers()->getId(),
'organization_id' => $data['organization']->getId(),
'ip' => $data['ip'] ?? null,
'user_accepted' => $removedUser->getId(),
'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM),
]);
break; break;
case 'USER_DELETED': case 'USER_DELETED':
if ($uoa && $adminUO->getUsers()->getId() !== $data['user']->getId() ) { if ($uoa && $adminUO->getUsers()->getId() !== $data['user']->getId() ) {
@ -150,14 +143,11 @@ class OrganizationsService
$removedUser, $removedUser,
$data['organization'] $data['organization']
); );
$this->loggerService->logAdminNotified([
'admin_user_id' =>$adminUO->getUsers()->getId(),
'target_user_id' => $newUser->getId(),
'organization_id' => $data['organization']->getId(),'case' =>$type]);
} }
$this->emailNotificationLogger->info('Organization admins notified of user deleted', [
'admin_user_id' => $adminUO->getUsers()->getId(),
'organization_id' => $data['organization']->getId(),
'ip' => $data['ip'] ?? null,
'user_accepted' => $removedUser->getId(),
'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM),
]);
break; break;
case 'USER_ACTIVATED': case 'USER_ACTIVATED':
if ($uoa && $adminUO->getUsers()->getId() !== $data['user']->getId() ) { if ($uoa && $adminUO->getUsers()->getId() !== $data['user']->getId() ) {
@ -167,14 +157,11 @@ class OrganizationsService
$activatedUser, $activatedUser,
$data['organization'] $data['organization']
); );
$this->loggerService->logAdminNotified([
'admin_user_id' =>$adminUO->getUsers()->getId(),
'target_user_id' => $newUser->getId(),
'organization_id' => $data['organization']->getId(),'case' =>$type]);
} }
$this->emailNotificationLogger->info('Organization admins notified of user activated', [
'admin_user_id' => $adminUO->getUsers()->getId(),
'organization_id' => $data['organization']->getId(),
'ip' => $data['ip'] ?? null,
'user_accepted' => $activatedUser->getId(),
'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM),
]);
break; break;
} }

View File

@ -302,34 +302,6 @@ class UserService
return 'ROLE_' . $role; return 'ROLE_' . $role;
} }
public function revokeUserTokens(string $userIdentifier)
{
$tokens = $this->entityManager->getRepository(AccessToken::class)->findBy([
'userIdentifier' => $userIdentifier,
'revoked' => false
]);
foreach ($tokens as $token) {
try{
$token->revoke();
$this->loggerService->logTokenRevocation(
'Access token revoked for user',
[
'user_identifier' => $userIdentifier,
'token_id' => $token->getIdentifier(),
]
);
}catch (\Exception $e){
$this->loggerService->logError(
'Error revoking access token: ' . $e->getMessage(),
[
'user_identifier' => $userIdentifier,
'token_id' => $token->getIdentifier(),
]
);
}
}
}
public function formatStatutForOrganizations(array $rows): array public function formatStatutForOrganizations(array $rows): array
{ {
@ -626,13 +598,14 @@ class UserService
$token = $this->generatePasswordToken($user, $org->getId()); $token = $this->generatePasswordToken($user, $org->getId());
$this->emailService->sendExistingUserNotificationEmail($user, $org, $token); $this->emailService->sendExistingUserNotificationEmail($user, $org, $token);
$this->loggerService->logExistingUserNotificationSent($user->getId(), $org->getId()); $this->loggerService->logExistingUserNotificationSent($user->getId(), $org->getId());
$this->organizationsService->notifyOrganizationAdmins(['user'=> $user, 'acting_user_id'=>$actingUser->getId(),
'organization'=> $org], 'USER_INVITED');
} catch (\Exception $e) { } catch (\Exception $e) {
$this->loggerService->logError("Error sending existing user notification: " . $e->getMessage(), [ $this->loggerService->logError("Error sending existing user notification: " . $e->getMessage(), [
'target_user_id' => $user->getId(), 'target_user_id' => $user->getId(),
'organization_id' => $org->getId(), 'organization_id' => $org->getId(),
]); ]);
} }
$this->notifyOrgAdmins($user, $org, $actingUser,);
} }
private function sendNewUserNotifications(User $user, Organizations $org, User $actingUser): void private function sendNewUserNotifications(User $user, Organizations $org, User $actingUser): void
@ -640,37 +613,14 @@ class UserService
try { try {
$token = $this->generatePasswordToken($user, $org->getId()); $token = $this->generatePasswordToken($user, $org->getId());
$this->emailService->sendPasswordSetupEmail($user, $token); $this->emailService->sendPasswordSetupEmail($user, $token);
$this->organizationsService->notifyOrganizationAdmins(['user'=> $user, 'acting_user_id'=>$actingUser->getId(),
'organization'=> $org], 'USER_INVITED');
} catch (\Exception $e) { } catch (\Exception $e) {
$this->loggerService->logError("Error sending password setup email: " . $e->getMessage(), [ $this->loggerService->logError("Error sending password setup email: " . $e->getMessage(), [
'target_user_id' => $user->getId(), 'target_user_id' => $user->getId(),
'organization_id' => $org->getId(), 'organization_id' => $org->getId(),
]); ]);
} }
$this->notifyOrgAdmins($user, $org, $actingUser);
}
private function notifyOrgAdmins(User $user, Organizations $org, User $actingUser): void
{
try {
$data = ['user' => $user,
'organization' => $org];
$adminsUos = $this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED');
foreach ($adminsUos as $adminUo) {
$this->loggerService->logAdminNotified(
$adminUo->getUsers()->getId(),
$user->getId(),
$org->getId(),
$actingUser->getId(),
);
}
} catch (\Exception $e) {
$this->loggerService->logError("Error notifying organization admins: " . $e->getMessage(), [
'target_user_id' => $user->getId(),
'organization_id' => $org->getId(),
]);
}
} }
} }