Merge remote-tracking branch 'origin/master'

pull/5206/head
Angel Fernando Quiroz Campos 2 years ago
commit 5ef6744f32
  1. 11
      src/CoreBundle/Controller/SecurityController.php
  2. 3
      src/CoreBundle/Entity/TrackELoginRecord.php
  3. 15
      src/CoreBundle/EventListener/LoginSuccessHandler.php
  4. 22
      src/CoreBundle/EventSubscriber/LoginFailureSubscriber.php
  5. 14
      src/CoreBundle/Repository/TrackELoginRecordRepository.php
  6. 2
      src/CoreBundle/Resources/config/listeners.yml
  7. 122
      src/CoreBundle/ServiceHelper/LoginAttemptLogger.php

@ -98,17 +98,6 @@ class SecurityController extends AbstractController
$data = null;
if ($user) {
// Log of connection attempts
$trackELoginRecord = new TrackELoginRecord();
$trackELoginRecord
->setUsername($user->getUsername())
->setLoginDate(new DateTime())
->setUserIp(api_get_real_ip())
->setSuccess(true)
;
$this->trackELoginRecordRepository->create($trackELoginRecord);
$data = $this->serializer->serialize($user, 'jsonld', ['groups' => ['user:read']]);
}

@ -6,6 +6,7 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Entity;
use Chamilo\CoreBundle\Repository\TrackELoginRecordRepository;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
@ -13,7 +14,7 @@ use Doctrine\ORM\Mapping as ORM;
* Track Login Record.
*/
#[ORM\Table(name: 'track_e_login_record')]
#[ORM\Entity]
#[ORM\Entity(repositoryClass: TrackELoginRecordRepository::class)]
class TrackELoginRecord
{
#[ORM\Column(name: 'id', type: 'integer')]

@ -7,10 +7,13 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\EventListener;
use Chamilo\CoreBundle\Entity\TrackELogin;
use Chamilo\CoreBundle\Entity\TrackELoginRecord;
use Chamilo\CoreBundle\Entity\TrackEOnline;
use Chamilo\CoreBundle\Entity\User;
use Chamilo\CoreBundle\Repository\TrackELoginRecordRepository;
use Chamilo\CoreBundle\Repository\TrackELoginRepository;
use Chamilo\CoreBundle\Repository\TrackEOnlineRepository;
use Chamilo\CoreBundle\ServiceHelper\LoginAttemptLogger;
use Chamilo\CoreBundle\Settings\SettingsManager;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
@ -27,17 +30,20 @@ class LoginSuccessHandler
protected AuthorizationCheckerInterface $checker;
protected SettingsManager $settingsManager;
protected EntityManagerInterface $entityManager;
private LoginAttemptLogger $loginAttemptLogger;
public function __construct(
UrlGeneratorInterface $urlGenerator,
AuthorizationCheckerInterface $checker,
SettingsManager $settingsManager,
EntityManagerInterface $entityManager
EntityManagerInterface $entityManager,
LoginAttemptLogger $loginAttemptLogger
) {
$this->router = $urlGenerator;
$this->checker = $checker;
$this->settingsManager = $settingsManager;
$this->entityManager = $entityManager;
$this->loginAttemptLogger = $loginAttemptLogger;
}
/**
@ -147,9 +153,16 @@ class LoginSuccessHandler
/** @var TrackELoginRepository $trackELoginRepository */
$trackELoginRepository = $this->entityManager->getRepository(TrackELogin::class);
/** @var TrackELoginRecordRepository $trackELoginRecordRepository */
$trackELoginRecordRepository = $this->entityManager->getRepository(TrackELoginRecord::class);
$trackELoginRepository->createLoginRecord($user, new DateTime(), $userIp);
$trackEOnlineRepository->createOnlineSession($user, $userIp);
// Log of connection attempts
$trackELoginRecordRepository->addTrackLogin($user->getUsername(), $userIp, true);
$this->loginAttemptLogger->logAttempt(true, $user->getUsername(), $userIp);
$session->set('login_records_created', true);
}

@ -6,17 +6,19 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\EventSubscriber;
use Chamilo\CoreBundle\Entity\TrackELoginRecord;
use Chamilo\CoreBundle\Repository\TrackELoginRecordRepository;
use DateTime;
use Chamilo\CoreBundle\ServiceHelper\LoginAttemptLogger;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
class LoginFailureSubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly TrackELoginRecordRepository $trackELoginRecordingRepository
private readonly TrackELoginRecordRepository $trackELoginRecordingRepository,
private readonly RequestStack $requestStack,
private readonly LoginAttemptLogger $loginAttemptLogger
) {}
public static function getSubscribedEvents(): array
@ -28,20 +30,16 @@ class LoginFailureSubscriber implements EventSubscriberInterface
public function onFailureEvent(LoginFailureEvent $event): void
{
$passport = $event->getPassport();
$request = $this->requestStack->getCurrentRequest();
$userIp = $request ? $request->getClientIp() : 'unknown';
$passport = $event->getPassport();
/** @var UserBadge $userBadge */
$userBadge = $passport->getBadge(UserBadge::class);
$username = $userBadge->getUserIdentifier();
// Log of connection attempts
$trackELoginRecord = new TrackELoginRecord();
$trackELoginRecord
->setUsername($username)
->setLoginDate(new DateTime())
->setUserIp(api_get_real_ip())
->setSuccess(false)
;
$this->trackELoginRecordingRepository->create($trackELoginRecord);
$this->trackELoginRecordingRepository->addTrackLogin($username, $userIp, false);
$this->loginAttemptLogger->logAttempt(false, $username, $userIp);
}
}

@ -7,6 +7,7 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Repository;
use Chamilo\CoreBundle\Entity\TrackELoginRecord;
use DateTime;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
@ -17,9 +18,16 @@ final class TrackELoginRecordRepository extends ServiceEntityRepository
parent::__construct($registry, TrackELoginRecord::class);
}
public function create(TrackELoginRecord $trackELoginRecord): void
public function addTrackLogin(string $username, string $userIp, bool $success): void
{
$this->getEntityManager()->persist($trackELoginRecord);
$this->getEntityManager()->flush();
$trackELoginRecord = new TrackELoginRecord();
$trackELoginRecord
->setUsername($username)
->setLoginDate(new DateTime())
->setUserIp($userIp)
->setSuccess($success);
$this->_em->persist($trackELoginRecord);
$this->_em->flush();
}
}

@ -64,7 +64,7 @@ services:
# Auth listeners
Chamilo\CoreBundle\EventListener\LoginSuccessHandler:
arguments: ['@router', '@security.authorization_checker', '@Chamilo\CoreBundle\Settings\SettingsManager', '@doctrine.orm.entity_manager']
arguments: ['@router', '@security.authorization_checker', '@Chamilo\CoreBundle\Settings\SettingsManager', '@doctrine.orm.entity_manager', '@Chamilo\CoreBundle\ServiceHelper\LoginAttemptLogger']
tags:
- {name: kernel.event_listener, event: security.interactive_login, method: onSecurityInteractiveLogin}

@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace Chamilo\CoreBundle\ServiceHelper;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use DateTime;
use SplFileObject;
class LoginAttemptLogger
{
private string $logDir;
private TranslatorInterface $translator;
private int $maxLogs;
public function __construct(KernelInterface $kernel, TranslatorInterface $translator)
{
$this->logDir = $kernel->getLogDir() . '/ids';
$this->translator = $translator;
$this->maxLogs = 90; // for how many iterations to keep log files
if (!is_dir($this->logDir)) {
mkdir($this->logDir, 0777, true);
}
}
public function logAttempt(bool $status, string $username, string $ip): void
{
$logDir = $this->logDir;
if (!is_dir($logDir)) {
mkdir($logDir, 0777, true);
}
$date = new DateTime();
$logFilePrefix = $logDir . '/ids';
$logFile = $logFilePrefix . '.log';
// Most of the time, there will be a previous log file
if (file_exists($logFile)) {
// Read the last line of the existing log file to determine if we need rotation
$lastLine = $this->_readLastLine($logFile);
if (!$this->_checkDateIsToday($lastLine)) {
// The last line's date is not today, so we need to rotate
$this->_rotateLogFiles($logFile, $this->maxLogs);
}
}
$statusText = $this->translator->trans($status ? 'succeeded' : 'failed');
$infoText = $this->translator->trans('info');
$clientText = $this->translator->trans('client');
$loginMessage = $this->translator->trans('Login %status% for username %username%', ['%status%' => $statusText, '%username%' => $username]);
$logMessage = sprintf("[%s] [%s] [%s %s] %s\n",
$date->format('Y-m-d H:i:s'),
$infoText,
$clientText,
$ip,
$loginMessage
);
file_put_contents($logFile, $logMessage, FILE_APPEND | LOCK_EX);
}
/**
* Efficiently read the last line of the provided file, or an empty string
* @param string $logFilePath
* @return string
*/
private function _readLastLine(string $logFilePath): string
{
$fileObject = new SplFileObject($logFilePath, 'r');
$fileObject->seek(PHP_INT_MAX);
$fileObject->seek($fileObject->key() - 1);
$line = $fileObject->current();
if (empty($line)) {
return '';
}
return $line;
}
/**
* Check if the date in a log line is same as today
* @param string $line A line of type "[2024-03-01 09:44:57] [info] [client 127.0.0.1] Some text"
*/
private function _checkDateIsToday(string $line): bool
{
$date = new DateTime();
$today = $date->format('Y-m-d');
$matches = [];
if (!empty($line) && preg_match('/\[(\d{4}-\d{2}-\d{2})\s.*/', $line, $matches)) {
if (0 === strcmp($matches[1], $today)) {
return true;
}
}
return false;
}
/**
* Rotate log files
* @param string $baseLogFile
* @param int $maxLogs
* @return void
*/
private function _rotateLogFiles(string $baseLogFile, int $maxLogs): void
{
for ($i = $maxLogs; $i > 0; $i--) {
$oldLog = $baseLogFile.'.'.$i;
$newLog = $baseLogFile.'.'.($i + 1);
if (file_exists($oldLog)) {
if (file_exists($newLog)) {
unlink($newLog);
}
rename($oldLog, $newLog);
}
}
rename($baseLogFile, $baseLogFile.'.1');
}
}
Loading…
Cancel
Save