src/Platform/SecurityBundle/Service/Firewalls/DashboardSecuritySubscriber.php line 327

Open in your IDE?
  1. <?php
  2. namespace Platform\SecurityBundle\Service\Firewalls;
  3. use Cms\CoreBundle\Util\Doctrine\EntityManager;
  4. use Cms\FrontendBundle\Service\FrontendBuilder;
  5. use Cms\TenantBundle\Entity\Tenant;
  6. use Cms\TenantBundle\Model\ProductsBitwise;
  7. use Platform\SecurityBundle\Entity\Identity\Account;
  8. use Platform\SecurityBundle\Entity\Login\Attempt;
  9. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  10. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  11. use Symfony\Component\HttpFoundation\Cookie;
  12. use Symfony\Component\HttpFoundation\RedirectResponse;
  13. use Symfony\Component\Routing\RouterInterface;
  14. use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
  15. use Symfony\Component\Security\Core\Exception\AuthenticationException;
  16. use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
  17. use Symfony\Component\Security\Http\Event\LoginFailureEvent;
  18. use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
  19. use Symfony\Component\Security\Http\Event\LogoutEvent;
  20. /**
  21.  * TODO: have this registered to the dashboard firewalls dispatcher...
  22.  * Handles various security event handling relating to the "dashboard" firewall specifically.
  23.  */
  24. final class DashboardSecuritySubscriber implements EventSubscriberInterface
  25. {
  26.     /**
  27.      * @var ParameterBagInterface
  28.      */
  29.     private ParameterBagInterface $params;
  30.     /**
  31.      * @var EntityManager
  32.      */
  33.     private EntityManager $em;
  34.     /**
  35.      * @var RouterInterface
  36.      */
  37.     private RouterInterface $router;
  38.     /**
  39.      * @param ParameterBagInterface $params
  40.      * @param EntityManager $em
  41.      * @param RouterInterface $router
  42.      */
  43.     public function __construct(
  44.         ParameterBagInterface $params,
  45.         EntityManager $em,
  46.         RouterInterface $router
  47.     )
  48.     {
  49.         $this->params $params;
  50.         $this->em $em;
  51.         $this->router $router;
  52.     }
  53.     /**
  54.      * {@inheritDoc}
  55.      */
  56.     public static function getSubscribedEvents(): array
  57.     {
  58.         return [
  59.             AuthenticationSuccessEvent::class => [
  60.                 ['onAuthenticationSuccessCheckSystemStatus'0],
  61.                 ['onAuthenticationSuccessCheckTenantStatus'0],
  62.                 ['onAuthenticationSuccessCheckAttempts'0],
  63.                 ['onAuthenticationSuccessCheckUserStatus'0],
  64.             ],
  65.             LoginFailureEvent::class => [
  66.                 ['onLoginFailureRedirectToLogin'0],
  67.             ],
  68.             LoginSuccessEvent::class => [
  69.                 ['onLoginSuccessLastLoggedInUpdate'0],
  70.                 ['onLoginSuccessRedirection'0],
  71.             ],
  72.             LogoutEvent::class => [
  73.                 ['onLogout'0],
  74.             ],
  75.         ];
  76.     }
  77.     /**
  78.      * Handles checking whether logins for the entire system are enabled/disabled.
  79.      * This is reserved for future use; there may be times when we need to ensure people cannot log in to the system.
  80.      *
  81.      * @param AuthenticationSuccessEvent $event
  82.      * @return void
  83.      */
  84.     public function onAuthenticationSuccessCheckSystemStatus(AuthenticationSuccessEvent $event): void
  85.     {
  86.         // get the user
  87.         $user $event->getAuthenticationToken()->getUser();
  88.         if ( ! $user instanceof Account) {
  89.             return;
  90.         }
  91.         // allow csadmin to continue to log in while in this state
  92.         if ($user->getEmail() === $this->params->get('app.security.csadmin.username')) {
  93.             return;
  94.         }
  95.         // NOOP: reserved for future use...
  96.         if (false) {
  97.             throw new AuthenticationException(
  98.                 'Logins to the system are currently disabled.',
  99.             );
  100.         }
  101.     }
  102.     /**
  103.      * Checks that the tenant attached to the user is in an operable status.
  104.      *
  105.      * @param AuthenticationSuccessEvent $event
  106.      * @return void
  107.      */
  108.     public function onAuthenticationSuccessCheckTenantStatus(AuthenticationSuccessEvent $event): void
  109.     {
  110.         // get the user
  111.         $user $event->getAuthenticationToken()->getUser();
  112.         if ( ! $user instanceof Account) {
  113.             return;
  114.         }
  115.         // allow csadmin to continue to log in while in this state
  116.         if ($user->getEmail() === $this->params->get('app.security.csadmin.username')) {
  117.             return;
  118.         }
  119.         // tenant must be marked as "ok" in order for logins to continue to function
  120.         if ($user->getTenant()->getStatus() !== Tenant::STATUS__OK) {
  121.             throw new AuthenticationException(
  122.                 'Logins to this instance are currently disabled.',
  123.             );
  124.         }
  125.     }
  126.     /**
  127.      * TODO: should probably add a tenant setting/property to control this check, in the events when customers goof and need to get in...
  128.      * Checks whether too many failed logins have been attempted for the given instance.
  129.      *
  130.      * @param AuthenticationSuccessEvent $event
  131.      * @return void
  132.      */
  133.     public function onAuthenticationSuccessCheckAttempts(AuthenticationSuccessEvent $event): void
  134.     {
  135.         // get the user
  136.         $user $event->getAuthenticationToken()->getUser();
  137.         if ( ! $user instanceof Account) {
  138.             return;
  139.         }
  140.         // allow csadmin to continue to log in while in this state
  141.         if ($user->getEmail() === $this->params->get('app.security.csadmin.username')) {
  142.             return;
  143.         }
  144.         // count how many failed logins for this customer in the last bit of time
  145.         $count $this->em->getRepository(Attempt::class)->countLastHourFailed(
  146.             $user->getTenant(),
  147.         );
  148.         // check if the account exceeds our threshold
  149.         if ($count 250) {
  150.             throw new AuthenticationException(
  151.                 'Logins for this instance are currently disabled.',
  152.             );
  153.         }
  154.     }
  155.     /**
  156.      * Checks that the user is a proper, active status when logging in.
  157.      *
  158.      * @param AuthenticationSuccessEvent $event
  159.      * @return void
  160.      */
  161.     public function onAuthenticationSuccessCheckUserStatus(AuthenticationSuccessEvent $event): void
  162.     {
  163.         // get the user
  164.         $user $event->getAuthenticationToken()->getUser();
  165.         if ( ! $user instanceof Account) {
  166.             return;
  167.         }
  168.         // if the account is not active, we need to reject the login
  169.         if ( ! $user->isActive()) {
  170.             throw new AuthenticationException(
  171.                 'Logins to this account are currently disabled.',
  172.             );
  173.         }
  174.     }
  175.     /**
  176.      * @param LoginFailureEvent $event
  177.      * @return void
  178.      */
  179.     public function onLoginFailureRedirectToLogin(LoginFailureEvent $event): void
  180.     {
  181.         // ensure proper firewall
  182.         if ($event->getFirewallName() !== 'dashboard') {
  183.             return;
  184.         }
  185.         // redirect to the login selection page
  186.         // TODO: is this necessary; might be a setting in the yaml that can take care of this...
  187.         $event->setResponse(
  188.             new RedirectResponse(
  189.                 $this->router->generate('platform.security.login.default.select'),
  190.             ),
  191.         );
  192.     }
  193.     /**
  194.      * @param LoginSuccessEvent $event
  195.      * @return void
  196.      */
  197.     public function onLoginSuccessLastLoggedInUpdate(LoginSuccessEvent $event): void
  198.     {
  199.         // ensure proper firewall
  200.         if ($event->getFirewallName() !== 'dashboard') {
  201.             return;
  202.         }
  203.         // get the user
  204.         $user $event->getUser();
  205.         if ( ! $user instanceof Account) {
  206.             throw new \RuntimeException();
  207.         }
  208.         // mark the user with the latest timestamp
  209.         $this->em->getRepository(Account::class)->updateLastLoginTimestamp(
  210.             $user,
  211.         );
  212.         // refresh from the DB the current $user object
  213.         $this->em->refresh($user);
  214.     }
  215.     /**
  216.      * @param LoginSuccessEvent $event
  217.      * @return void
  218.      */
  219.     public function onLoginSuccessRedirection(LoginSuccessEvent $event): void
  220.     {
  221.         // checks we need to run
  222.         // NOTE: order matters!
  223.         static $checks = [
  224.             ProductsBitwise::SITES__BASE => null,
  225.             ProductsBitwise::NOTIFICATIONS__V2 => 'app.notifications.dashboard.default.main',
  226.             ProductsBitwise::ACCESSIBILITY__CONTENT => 'products.ada.dashboard.default.main',
  227.             ProductsBitwise::ACCESSIBILITY__PDFS => 'products.ada.dashboard.default.main',
  228.             ProductsBitwise::SMM__BASE => 'products.smm.dashboard.default.main',
  229.             ProductsBitwise::APP__CAMPUSSUITE => 'products.app.dashboard.default.main',
  230.             ProductsBitwise::APP__SIA => 'products.app.dashboard.default.main',
  231.         ];
  232.         // ensure proper firewall
  233.         if ($event->getFirewallName() !== 'dashboard') {
  234.             return;
  235.         }
  236.         // get the user
  237.         $user $event->getUser();
  238.         if ( ! $user instanceof Account) {
  239.             throw new \RuntimeException();
  240.         }
  241.         // first see if we have a redirect stored in the passport (in case it was set via oauth or something)
  242.         $passport $event->getPassport();
  243.         $redirect null;
  244.         if ($passport instanceof Passport) {
  245.             $redirect $passport->getAttribute('oauth_redirect');
  246.         }
  247.         // see if we have a redirect given in the authentication flow, if not already given
  248.         $redirect $redirect ?: $event->getRequest()->query->get('redirect');
  249.         // determine where to go to, either redirect or dashboard
  250.         if ($redirect === null || $redirect === '' || $redirect === '/') {
  251.             // single out customers that have single-product instances to jump to those specific products
  252.             foreach ($checks as $product => $route) {
  253.                 if ($user->getTenant()->getProducts()->checkAnyFlag($product)) {
  254.                     $redirect $route
  255.                         $this->router->generate($route)
  256.                         : null;
  257.                     break;
  258.                 }
  259.             }
  260.             // if we still don't have a redirect, determine the best place to put them
  261.             if (empty($redirect)) {
  262.                 switch (true) {
  263.                     case $user->getTenant()->getProducts()->hasFlag(ProductsBitwise::SCHOOLNOW__MIGRATED):
  264.                         $redirect $this->router->generate('app.schoolnow.dashboard.default.main');
  265.                         break;
  266.                     default:
  267.                         $redirect $this->router->generate('cms.container.dashboard.dashboard.index');
  268.                 }
  269.             }
  270.         }
  271.         // force https, should always be redirecting to a dashboard url anyway
  272.         $pos strpos($redirect'http://');
  273.         if ($pos === 0) {
  274.             $redirect substr_replace($redirect'https://'$posstrlen('http://'));
  275.         }
  276.         // generate redirect to the dashboard
  277.         $response = new RedirectResponse($redirect);
  278.         // set user uid cookie
  279.         // TODO: is this necessary anymore?
  280.         $response->headers->setCookie(new Cookie(
  281.             FrontendBuilder::ACCOUNT_UID_KEY,
  282.             $user->getUid()->toString(),
  283.             0'/'nullfalsefalse
  284.         ));
  285.         // set the response on the event
  286.         $event->setResponse($response);
  287.     }
  288.     /**
  289.      * @param LogoutEvent $event
  290.      * @return void
  291.      */
  292.     public function onLogout(LogoutEvent $event): void
  293.     {
  294.         if ($event->getRequest() && $event->getRequest()->getSession()) {
  295.             $event->getRequest()->getSession()->invalidate();
  296.         }
  297.     }
  298. }