diff --git a/lib/RoadizCompatBundle/src/Aliases.php b/lib/RoadizCompatBundle/src/Aliases.php index c26a8346..cf8a221b 100644 --- a/lib/RoadizCompatBundle/src/Aliases.php +++ b/lib/RoadizCompatBundle/src/Aliases.php @@ -66,7 +66,6 @@ public static function getAliases(): array \RZ\Roadiz\CoreBundle\Entity\FolderTranslation::class => \RZ\Roadiz\Core\Entities\FolderTranslation::class, \RZ\Roadiz\CoreBundle\Entity\Group::class => \RZ\Roadiz\Core\Entities\Group::class, \RZ\Roadiz\CoreBundle\Entity\Log::class => \RZ\Roadiz\Core\Entities\Log::class, - \RZ\Roadiz\CoreBundle\Entity\LoginAttempt::class => \RZ\Roadiz\Core\Entities\LoginAttempt::class, \RZ\Roadiz\CoreBundle\Entity\Node::class => \RZ\Roadiz\Core\Entities\Node::class, \RZ\Roadiz\CoreBundle\Entity\NodeType::class => \RZ\Roadiz\Core\Entities\NodeType::class, \RZ\Roadiz\CoreBundle\Entity\NodeTypeField::class => \RZ\Roadiz\Core\Entities\NodeTypeField::class, diff --git a/lib/RoadizCoreBundle/config/services.yaml b/lib/RoadizCoreBundle/config/services.yaml index 0fe7493a..a172ae7f 100644 --- a/lib/RoadizCoreBundle/config/services.yaml +++ b/lib/RoadizCoreBundle/config/services.yaml @@ -449,7 +449,6 @@ services: Solarium\Core\Client\Client: factory: ['RZ\Roadiz\CoreBundle\SearchEngine\ClientRegistry', 'getClient'] - # Provides LoginAttemptManager aware authentication RZ\Roadiz\CoreBundle\Security\Authentication\JwtAuthenticationSuccessHandler: decorates: 'lexik_jwt_authentication.handler.authentication_success' diff --git a/lib/RoadizCoreBundle/migrations/Version20230712171650.php b/lib/RoadizCoreBundle/migrations/Version20230712171650.php new file mode 100644 index 00000000..cf03d449 --- /dev/null +++ b/lib/RoadizCoreBundle/migrations/Version20230712171650.php @@ -0,0 +1,36 @@ +addSql('DROP TABLE login_attempts'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE TABLE login_attempts (id INT AUTO_INCREMENT NOT NULL, ip_address VARCHAR(46) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, date DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', blocks_login_until DATETIME DEFAULT NULL, username VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, attempt_count INT DEFAULT NULL, INDEX IDX_9163C7FBEFF8A4EEF85E0677 (blocks_login_until, username), INDEX IDX_9163C7FBEFF8A4EEF85E067722FFD58C (blocks_login_until, username, ip_address), INDEX IDX_9163C7FBF85E0677 (username), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB COMMENT = \'\' '); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/lib/RoadizCoreBundle/src/Console/CleanLoginAttemptCommand.php b/lib/RoadizCoreBundle/src/Console/CleanLoginAttemptCommand.php deleted file mode 100644 index 758ac145..00000000 --- a/lib/RoadizCoreBundle/src/Console/CleanLoginAttemptCommand.php +++ /dev/null @@ -1,43 +0,0 @@ -managerRegistry = $managerRegistry; - } - - protected function configure(): void - { - $this->setName('login-attempts:clean') - ->setDescription('Clean all login attempts older than 1 day'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - - $this->managerRegistry->getRepository(LoginAttempt::class)->cleanLoginAttempts(); - - $io->success('All login attempts older than 1 day were deleted.'); - - return 0; - } -} diff --git a/lib/RoadizCoreBundle/src/Console/PurgeLoginAttemptCommand.php b/lib/RoadizCoreBundle/src/Console/PurgeLoginAttemptCommand.php deleted file mode 100644 index 95985673..00000000 --- a/lib/RoadizCoreBundle/src/Console/PurgeLoginAttemptCommand.php +++ /dev/null @@ -1,54 +0,0 @@ -managerRegistry = $managerRegistry; - } - - protected function configure(): void - { - $this->setName('login-attempts:purge') - ->setDescription('Purge all login attempts for one IP address') - ->addArgument( - 'ip-address', - InputArgument::REQUIRED - ); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - $this->managerRegistry - ->getRepository(LoginAttempt::class) - ->purgeLoginAttempts($input->getArgument('ip-address')); - - $io->success('All login attempts were deleted for ' . $input->getArgument('ip-address')); - - return 0; - } -} diff --git a/lib/RoadizCoreBundle/src/Entity/LoginAttempt.php b/lib/RoadizCoreBundle/src/Entity/LoginAttempt.php deleted file mode 100644 index 68eb84c8..00000000 --- a/lib/RoadizCoreBundle/src/Entity/LoginAttempt.php +++ /dev/null @@ -1,109 +0,0 @@ -ipAddress = $ipAddress; - $this->username = $username; - $this->date = new \DateTimeImmutable('now'); - $this->blocksLoginUntil = new \DateTime('now'); - $this->attemptCount = 0; - } - - public function getId(): int - { - return $this->id; - } - - public function getIpAddress(): ?string - { - return $this->ipAddress; - } - - public function getDate(): \DateTimeImmutable - { - return $this->date; - } - - public function getUsername(): ?string - { - return $this->username; - } - - /** - * @return \DateTime|null - */ - public function getBlocksLoginUntil(): ?\DateTime - { - return $this->blocksLoginUntil; - } - - /** - * @param \DateTime $blocksLoginUntil - * - * @return LoginAttempt - */ - public function setBlocksLoginUntil(\DateTime $blocksLoginUntil): LoginAttempt - { - $this->blocksLoginUntil = $blocksLoginUntil; - - return $this; - } - - /** - * @return int - */ - public function getAttemptCount(): int - { - return $this->attemptCount; - } - - /** - * @return LoginAttempt - */ - public function addAttemptCount(): LoginAttempt - { - $this->attemptCount++; - return $this; - } -} diff --git a/lib/RoadizCoreBundle/src/Exception/TooManyLoginAttemptsException.php b/lib/RoadizCoreBundle/src/Exception/TooManyLoginAttemptsException.php deleted file mode 100644 index a15276b0..00000000 --- a/lib/RoadizCoreBundle/src/Exception/TooManyLoginAttemptsException.php +++ /dev/null @@ -1,18 +0,0 @@ - - */ -final class LoginAttemptRepository extends EntityRepository -{ - public function __construct( - ManagerRegistry $registry, - EventDispatcherInterface $dispatcher - ) { - parent::__construct($registry, LoginAttempt::class, $dispatcher); - } - - /** - * @param string $username - * - * @return bool - * @throws \Doctrine\ORM\NoResultException - * @throws \Doctrine\ORM\NonUniqueResultException - */ - public function isUsernameBlocked(string $username): bool - { - $qb = $this->createQueryBuilder('la'); - return $qb->select('COUNT(la)') - ->andWhere($qb->expr()->gte('la.blocksLoginUntil', ':now')) - ->andWhere($qb->expr()->eq('la.username', ':username')) - ->getQuery() - ->setParameters([ - 'now' => new \DateTime('now'), - 'username' => $username, - ]) - ->getSingleScalarResult() > 0 - ; - } - - /** - * Checks if an IP address tries more than 10 usernames - * in the last 5 minutes. - * - * @param string $ipAddress - * @param int $seconds - * @param int $count - * - * @return bool - * @throws \Doctrine\ORM\NoResultException - * @throws \Doctrine\ORM\NonUniqueResultException - */ - public function isIpAddressBlocked(string $ipAddress, int $seconds = 1200, int $count = 10): bool - { - $qb = $this->createQueryBuilder('la'); - $query = $qb->select('SUM(la.attemptCount)') - ->andWhere($qb->expr()->gte('la.date', ':now')) - ->andWhere($qb->expr()->eq('la.ipAddress', ':ipAddress')) - ->getQuery() - ->setParameters([ - 'now' => (new \DateTime())->sub(new \DateInterval('PT' . $seconds . 'S')), - 'ipAddress' => $ipAddress, - ]) - ; - return $query->getSingleScalarResult() > $count ? true : false; - } - - /** - * @param string $ipAddress - * @param string $username - * - * @return LoginAttempt - * @throws \Doctrine\ORM\ORMException - */ - public function findOrCreateOneByIpAddressAndUsername(string $ipAddress, string $username): LoginAttempt - { - /** @var LoginAttempt|null $loginAttempt */ - $loginAttempt = $this->findOneBy([ - 'ipAddress' => $ipAddress, - 'username' => $username, - ]); - if (null === $loginAttempt) { - $loginAttempt = new LoginAttempt($ipAddress, $username); - $this->_em->persist($loginAttempt); - } - - return $loginAttempt; - } - - /** - * @param string $ipAddress - * @param string $username - */ - public function resetLoginAttempts(string $ipAddress, string $username): void - { - $qb = $this->_em->createQueryBuilder(); - $qb->delete(LoginAttempt::class, 'la') - ->andWhere($qb->expr()->eq('la.ipAddress', ':ipAddress')) - ->andWhere($qb->expr()->eq('la.username', ':username')) - ->getQuery() - ->execute([ - 'username' => $username, - 'ipAddress' => $ipAddress, - ]) - ; - } - - /** - * @param string $ipAddress - */ - public function purgeLoginAttempts(string $ipAddress): void - { - $qb = $this->_em->createQueryBuilder(); - $qb->delete(LoginAttempt::class, 'la') - ->andWhere($qb->expr()->eq('la.ipAddress', ':ipAddress')) - ->getQuery() - ->execute([ - 'ipAddress' => $ipAddress, - ]) - ; - } - - public function cleanLoginAttempts(): void - { - $qb = $this->_em->createQueryBuilder(); - $qb->delete(LoginAttempt::class, 'la') - ->andWhere($qb->expr()->lte('la.blocksLoginUntil', ':date')) - ->getQuery() - ->execute([ - 'date' => (new \DateTime())->sub(new \DateInterval('P1D')), - ]) - ; - } -} diff --git a/lib/RoadizCoreBundle/src/Security/Authentication/Manager/LoginAttemptManager.php b/lib/RoadizCoreBundle/src/Security/Authentication/Manager/LoginAttemptManager.php deleted file mode 100644 index 5e1e7ad1..00000000 --- a/lib/RoadizCoreBundle/src/Security/Authentication/Manager/LoginAttemptManager.php +++ /dev/null @@ -1,160 +0,0 @@ -requestStack = $requestStack; - $this->logger = $logger; - $this->managerRegistry = $managerRegistry; - } - - /** - * @param string $username - * @throws \Doctrine\ORM\NoResultException - * @throws \Doctrine\ORM\NonUniqueResultException - */ - public function checkLoginAttempts(string $username): void - { - /* - * Checks if there are more than 10 failed attempts - * from same IP address in the last 20 minutes - */ - if ( - $this->getLoginAttemptRepository()->isIpAddressBlocked( - $this->requestStack->getMainRequest()->getClientIp(), - $this->getIpAttemptGraceTime(), - $this->getIpAttemptCount() - ) - ) { - throw new TooManyLoginAttemptsException( - 'Too many login attempts for current IP address, wait before trying again.', - Response::HTTP_TOO_MANY_REQUESTS - ); - } - if ($this->getLoginAttemptRepository()->isUsernameBlocked($username)) { - throw new TooManyLoginAttemptsException( - 'Too many login attempts for this username, wait before trying again.', - Response::HTTP_TOO_MANY_REQUESTS - ); - } - } - - /** - * @param string $username - * - * @return $this - * @throws \Exception - */ - public function onFailedLoginAttempt(string $username): LoginAttemptManager - { - $manager = $this->managerRegistry->getManagerForClass(LoginAttempt::class); - if (null === $manager) { - throw new \RuntimeException('No manager found for class ' . LoginAttempt::class); - } - $loginAttempt = $this->getLoginAttemptRepository()->findOrCreateOneByIpAddressAndUsername( - $this->requestStack->getMainRequest()->getClientIp(), - $username - ); - - $loginAttempt->addAttemptCount(); - $blocksUntil = new \DateTime(); - - if ($loginAttempt->getAttemptCount() >= 9) { - $blocksUntil->add(new \DateInterval('PT30M')); - $loginAttempt->setBlocksLoginUntil($blocksUntil); - $this->logger->info(sprintf( - 'Client has been blocked from login until %s', - $blocksUntil->format('Y-m-d H:i:s') - )); - } elseif ($loginAttempt->getAttemptCount() >= 6) { - $blocksUntil->add(new \DateInterval('PT15M')); - $loginAttempt->setBlocksLoginUntil($blocksUntil); - $this->logger->info(sprintf( - 'Client has been blocked from login until %s', - $blocksUntil->format('Y-m-d H:i:s') - )); - } elseif ($loginAttempt->getAttemptCount() >= 3) { - $blocksUntil->add(new \DateInterval('PT3M')); - $loginAttempt->setBlocksLoginUntil($blocksUntil); - $this->logger->info(sprintf( - 'Client has been blocked from login until %s', - $blocksUntil->format('Y-m-d H:i:s') - )); - } - $manager->flush(); - return $this; - } - - /** - * @return LoginAttemptRepository - */ - public function getLoginAttemptRepository(): LoginAttemptRepository - { - if (null === $this->loginAttemptRepository) { - $this->loginAttemptRepository = $this->managerRegistry->getRepository(LoginAttempt::class); - } - return $this->loginAttemptRepository; - } - - /** - * @param string $username - * - * @return $this - */ - public function onSuccessLoginAttempt(string $username) - { - $this->getLoginAttemptRepository()->resetLoginAttempts( - $this->requestStack->getMainRequest()->getClientIp(), - $username - ); - return $this; - } - - /** - * @return int - */ - public function getIpAttemptGraceTime(): int - { - return $this->ipAttemptGraceTime; - } - - /** - * @return int - */ - public function getIpAttemptCount(): int - { - return $this->ipAttemptCount; - } -} diff --git a/lib/Rozier/src/Traits/VersionedControllerTrait.php b/lib/Rozier/src/Traits/VersionedControllerTrait.php index f27acfe8..c595c3ce 100644 --- a/lib/Rozier/src/Traits/VersionedControllerTrait.php +++ b/lib/Rozier/src/Traits/VersionedControllerTrait.php @@ -5,9 +5,9 @@ namespace Themes\Rozier\Traits; use Gedmo\Exception\UnexpectedValueException; -use Gedmo\Loggable\Entity\Repository\LogEntryRepository; use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use RZ\Roadiz\CoreBundle\Entity\UserLogEntry; +use RZ\Roadiz\CoreBundle\Repository\UserLogEntryRepository; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -27,7 +27,6 @@ public function isReadOnly(): bool /** * @param bool $isReadOnly - * * @return self */ public function setIsReadOnly(bool $isReadOnly) @@ -40,9 +39,9 @@ public function setIsReadOnly(bool $isReadOnly) protected function handleVersions(Request $request, PersistableInterface $entity): ?Response { /** - * @var LogEntryRepository $repo + * @var UserLogEntryRepository $repo */ - $repo = $this->em()->getRepository(UserLogEntry::class); + $repo = $this->getDoctrine()->getRepository(UserLogEntry::class); $logs = $repo->getLogEntries($entity); $versionNumber = $request->get('version', null);