Commit 89580818 authored by João Ventura's avatar João Ventura Committed by Joao Ventura
Browse files

Issue #3316499 by jcnventura: Further code refinements to TfaLoginContextTrait

parent 4e4da3eb
Loading
Loading
Loading
Loading
+4 −20
Original line number Diff line number Diff line
@@ -4,10 +4,8 @@ namespace Drupal\tfa\Form;

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\tfa\Plugin\TfaSendInterface;
use Drupal\tfa\TfaLoginContextTrait;
use Drupal\tfa\TfaLoginTrait;
use Drupal\tfa\TfaUserDataTrait;
use Drupal\user\Form\UserLoginForm;
use Symfony\Component\DependencyInjection\ContainerInterface;

@@ -19,7 +17,6 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
class TfaLoginForm extends UserLoginForm {
  use TfaLoginContextTrait;
  use TfaLoginTrait;
  use TfaUserDataTrait;

  /**
   * Redirect destination service.
@@ -78,7 +75,9 @@ class TfaLoginForm extends UserLoginForm {
    }

    // Similar to tfa_user_login() but not required to force user logout.
    $this->setUser($uid);
    /** @var \Drupal\user\UserInterface $user */
    $user = $this->userStorage->load($uid);
    $this->setUser($user);

    /* Uncomment when things go wrong and you get logged out.
    user_login_finalize($user);
@@ -87,7 +86,7 @@ class TfaLoginForm extends UserLoginForm {
     */

    // Stop processing if Tfa is not enabled.
    if (!$this->isModuleSetup() || !$this->isTfaRequired()) {
    if ($this->isTfaDisabled()) {
      parent::submitForm($form, $form_state);
    }
    else {
@@ -119,8 +118,6 @@ class TfaLoginForm extends UserLoginForm {
    }
    else {
      // Begin TFA and set process context.
      // @todo This is used in send plugins which has not been implemented yet.
      // $this->begin($tfaValidationPlugin);
      if (!empty($this->getRequest()->query->get('destination'))) {
        $parameters = $this->destination->getAsArray();
        $this->getRequest()->query->remove('destination');
@@ -192,17 +189,4 @@ class TfaLoginForm extends UserLoginForm {
    }
  }

  /**
   * Begin the TFA process.
   *
   * @param \Drupal\tfa\Plugin\TfaSendInterface $tfaSendPlugin
   *   The send plugin instance.
   */
  protected function begin(TfaSendInterface $tfaSendPlugin) {
    // Invoke begin method on send validation plugins.
    if (method_exists($tfaSendPlugin, 'begin')) {
      $tfaSendPlugin->begin();
    }
  }

}
+56 −69
Original line number Diff line number Diff line
@@ -2,13 +2,19 @@

namespace Drupal\tfa;

use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\user\UserInterface;

/**
 * Provide context for the current login attempt.
 *
 * This class collects data needed to decide whether TFA is required and, if so,
 * This trait collects data needed to decide whether TFA is required and, if so,
 * whether it is successful. This includes configuration of the module, the
 * current request, and the user that is attempting to log in.
 *
 * The methods defined in this trait require that the user property is defined,
 * so make sure to call the setUser method before using any other method here.
 *
 * @internal
 */
trait TfaLoginContextTrait {
@@ -28,13 +34,6 @@ trait TfaLoginContextTrait {
   */
  protected $tfaLoginManager;

  /**
   * The tfaValidation plugin.
   *
   * @var \Drupal\tfa\Plugin\TfaValidationInterface|null
   */
  protected $tfaValidationPlugin;

  /**
   * Tfa settings config object.
   *
@@ -42,13 +41,6 @@ trait TfaLoginContextTrait {
   */
  protected $tfaSettings;

  /**
   * The user storage.
   *
   * @var \Drupal\user\UserStorageInterface
   */
  protected $userStorage;

  /**
   * Entity for the user that is attempting to login.
   *
@@ -56,36 +48,14 @@ trait TfaLoginContextTrait {
   */
  protected $user;

  /**
   * Array of login plugins for a given user.
   *
   * @var \Drupal\tfa\Plugin\TfaLoginInterface[]
   */
  protected $userLoginPlugins;

  /**
   * Array of login plugins.
   *
   * @var \Drupal\tfa\Plugin\TfaLoginInterface[]
   */
  protected $tfaLoginPlugins;

  /**
   * Set the user object.
   *
   * @param \Drupal\user\UserInterface $user
   *   The entity object of the user attempting to log in.
   */
  public function setUser($uid) {
    $this->user = $this->userStorage->load($uid);

    $this->tfaLoginPlugins = $this->tfaLoginManager->getPlugins(['uid' => $uid]);
    // If possible, set up an instance of tfaValidationPlugin and the user's
    // list of plugins.
    $validationPluginName = $this->tfaSettings->get('default_validation_plugin');
    if (!empty($validationPluginName)) {
      $this->tfaValidationPlugin = $this->tfaValidationManager
        ->createInstance($validationPluginName, ['uid' => $uid]);
      $this->userLoginPlugins = $this->tfaLoginManager
        ->getPlugins(['uid' => $uid]);
    }
  public function setUser(UserInterface $user) {
    $this->user = $user;
  }

  /**
@@ -102,29 +72,23 @@ trait TfaLoginContextTrait {
   * Is TFA enabled and configured?
   *
   * @return bool
   *   Whether or not the TFA module is configured for use.
   *   TRUE if TFA is disabled.
   */
  public function isModuleSetup() {
    return intval($this->tfaSettings->get('enabled')) && !empty($this->tfaSettings->get('default_validation_plugin'));
  public function isTfaDisabled() {
    // Global TFA settings take precedence.
    if (!($this->tfaSettings->get('enabled')) || empty($this->tfaSettings->get('default_validation_plugin'))) {
      return TRUE;
    }

  /**
   * Check whether $this->getUser() is required to use TFA.
   *
   * @return bool
   *   TRUE if $this->getUser() is required to use TFA.
   */
  public function isTfaRequired() {
    // If TFA has been set up for the user, then it is required.
    $user_tfa_data = $this->tfaGetTfaData($this->getUser()->id(), $this->userData);
    // Check if the user has enabled TFA.
    $user_tfa_data = $this->tfaGetTfaData($this->user->id(), $this->userData);
    if (!empty($user_tfa_data['status']) && !empty($user_tfa_data['data']['plugins'])) {
      return TRUE;
      return FALSE;
    }

    // If the user has a role that is required to use TFA, then return TRUE.
    // TFA is not necessary if the user doesn't have one of the required roles.
    $required_roles = array_filter($this->tfaSettings->get('required_roles'));
    $user_roles = $this->getUser()->getRoles();
    return (bool) array_intersect($required_roles, $user_roles);
    return empty(array_intersect($required_roles, $this->user->getRoles()));
  }

  /**
@@ -134,7 +98,23 @@ trait TfaLoginContextTrait {
   *   TRUE if Validation Plugin exists and is ready for use.
   */
  public function isReady() {
    return isset($this->tfaValidationPlugin) && $this->tfaValidationPlugin->ready();
    // If possible, set up an instance of tfaValidationPlugin and the user's
    // list of plugins.
    $default_validation_plugin = $this->tfaSettings->get('default_validation_plugin');
    if (!empty($default_validation_plugin)) {
      try {
        /** @var \Drupal\tfa\Plugin\TfaValidationInterface $validation_plugin */
        $validation_plugin = $this->tfaValidationManager->createInstance($default_validation_plugin, ['uid' => $this->user->id()]);
        if (isset($validation_plugin) && $validation_plugin->ready()) {
          return TRUE;
        }
      }
      catch (PluginException $e) {
        return FALSE;
      }
    }

    return FALSE;
  }

  /**
@@ -142,8 +122,7 @@ trait TfaLoginContextTrait {
   *
   * @return int|false
   *   FALSE if users are never allowed to log in without setting up TFA.
   *   The remaining number of times $this->getUser() may log in without setting
   *   up TFA.
   *   The remaining number of times user may log in without setting up TFA.
   */
  public function remainingSkips() {
    $allowed_skips = intval($this->tfaSettings->get('validation_skip'));
@@ -152,19 +131,19 @@ trait TfaLoginContextTrait {
      return FALSE;
    }

    $user_tfa_data = $this->tfaGetTfaData($this->getUser()->id(), $this->userData);
    $user_tfa_data = $this->tfaGetTfaData($this->user->id(), $this->userData);
    $validation_skipped = $user_tfa_data['validation_skipped'] ?? 0;
    return max(0, $allowed_skips - $validation_skipped);
  }

  /**
   * Increment the count of $this->getUser() logins without setting up TFA.
   * Increment the count of user logins without setting up TFA.
   */
  public function hasSkipped() {
    $user_tfa_data = $this->tfaGetTfaData($this->getUser()->id(), $this->userData);
    $user_tfa_data = $this->tfaGetTfaData($this->user->id(), $this->userData);
    $validation_skipped = $user_tfa_data['validation_skipped'] ?? 0;
    $user_tfa_data['validation_skipped'] = $validation_skipped + 1;
    $this->tfaSaveTfaData($this->getUser()->id(), $this->userData, $user_tfa_data);
    $this->tfaSaveTfaData($this->user->id(), $this->userData, $user_tfa_data);
  }

  /**
@@ -176,13 +155,21 @@ trait TfaLoginContextTrait {
   *   TRUE if login allowed otherwise FALSE.
   */
  public function pluginAllowsLogin() {
    if (!empty($this->tfaLoginPlugins)) {
      foreach ($this->tfaLoginPlugins as $plugin) {
    /** @var \Drupal\tfa\Plugin\TfaLoginInterface[] $login_plugins */
    try {
      $login_plugins = $this->tfaLoginManager->getPlugins(['uid' => $this->user->id()]);
    }
    catch (PluginException $e) {
      return FALSE;
    }
    if (!empty($login_plugins)) {
      foreach ($login_plugins as $plugin) {
        if ($plugin->loginAllowed()) {
          return TRUE;
        }
      }
    }

    return FALSE;
  }

@@ -191,7 +178,7 @@ trait TfaLoginContextTrait {
   */
  public function doUserLogin() {
    // @todo Set a hash mark to indicate TFA authorization has passed.
    user_login_finalize($this->getUser());
    user_login_finalize($this->user);
  }

}
+19 −29
Original line number Diff line number Diff line
@@ -105,7 +105,7 @@ class TfaContextTest extends UnitTestCase {
   */
  protected function getFixture() {
    // Use simple anonymous class to add the TfaLoginContextTrait.
    return new class($this->tfaValidationManager, $this->tfaLoginManager, $this->configFactory, $this->user, $this->userData, $this->userStorage) {
    return new class($this->tfaValidationManager, $this->tfaLoginManager, $this->configFactory, $this->userData, $this->userStorage) {
      use TfaLoginContextTrait;

      /**
@@ -117,21 +117,20 @@ class TfaContextTest extends UnitTestCase {
       *   The plugin manager for TFA login plugins.
       * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
       *   The configuration service.
       * @param \Drupal\user\UserInterface $user
       *   The user currently attempting to log in.
       * @param \Drupal\user\UserDataInterface $user_data
       *   The user data service.
       * @param \Drupal\user\UserStorageInterface $user_storage
       *   The user storage.
       */
      public function __construct(TfaValidationPluginManager $tfa_validation_manager, TfaLoginPluginManager $tfa_login_manager, ConfigFactoryInterface $config_factory, UserInterface $user, UserDataInterface $user_data, UserStorageInterface $user_storage) {
      public function __construct(TfaValidationPluginManager $tfa_validation_manager, TfaLoginPluginManager $tfa_login_manager, ConfigFactoryInterface $config_factory, UserDataInterface $user_data, UserStorageInterface $user_storage) {
        $this->tfaValidationManager = $tfa_validation_manager;
        $this->tfaLoginManager = $tfa_login_manager;
        $this->tfaSettings = $config_factory->get('tfa.settings');
        $this->userData = $user_data;
        $this->userStorage = $user_storage;

        $this->setUser(3);
        /** @var \Drupal\user\UserInterface $user */
        $user = $user_storage->load(3);
        $this->setUser($user);
      }

    };
@@ -146,28 +145,13 @@ class TfaContextTest extends UnitTestCase {
  }

  /**
   * @covers ::isModuleSetup
   * @covers ::isTfaDisabled
   */
  public function testIsModuleSetup() {
    // Defaults to false with empty mocked services.
  public function testIsTfaDisabled() {
    // Defaults to true with empty mocked services.
    $fixture = $this->getFixture();
    $this->assertFalse($fixture->isModuleSetup());
    $this->assertTrue($fixture->isTfaDisabled());

    // Enable.
    $settings = $this->prophesize(ImmutableConfig::class);
    $settings->get('enabled')->willReturn(TRUE);
    $settings->get('default_validation_plugin')->willReturn('foo');
    $config_factory = $this->prophesize(ConfigFactoryInterface::class);
    $config_factory->get('tfa.settings')->willReturn($settings->reveal());
    $this->configFactory = $config_factory->reveal();
    $fixture = $this->getFixture();
    $this->assertTrue($fixture->isModuleSetup());
  }

  /**
   * @covers ::isTfaRequired
   */
  public function testIsTfaRequired() {
    // User has setup TFA.
    $user_data = $this->prophesize(UserDataInterface::class);
    $user_data->get('tfa', 3, 'tfa_user_settings')->willReturn([
@@ -177,8 +161,14 @@ class TfaContextTest extends UnitTestCase {
      'validation_skipped' => 1,
    ]);
    $this->userData = $user_data->reveal();
    $settings = $this->prophesize(ImmutableConfig::class);
    $settings->get('enabled')->willReturn(TRUE);
    $settings->get('default_validation_plugin')->willReturn('foo');
    $config_factory = $this->prophesize(ConfigFactoryInterface::class);
    $config_factory->get('tfa.settings')->willReturn($settings->reveal());
    $this->configFactory = $config_factory->reveal();
    $fixture = $this->getFixture();
    $this->assertTrue($fixture->isTfaRequired());
    $this->assertFalse($fixture->isTfaDisabled());

    // Not setup, no required roles matching the user.
    $user_data->get('tfa', 3, 'tfa_user_settings')->willReturn([
@@ -189,6 +179,7 @@ class TfaContextTest extends UnitTestCase {
    ]);
    $this->userData = $user_data->reveal();
    $settings = $this->prophesize(ImmutableConfig::class);
    $settings->get('enabled')->willReturn(TRUE);
    $settings->get('default_validation_plugin')->willReturn('foo');
    $settings->get('required_roles')->willReturn(['foo' => 'foo']);
    $config_factory = $this->prophesize(ConfigFactoryInterface::class);
@@ -201,9 +192,8 @@ class TfaContextTest extends UnitTestCase {
    $user_storage = $this->prophesize(UserStorageInterface::class);
    $user_storage->load(3)->willReturn($this->user);
    $this->userStorage = $user_storage->reveal();

    $fixture = $this->getFixture();
    $this->assertFalse($fixture->isTfaRequired());
    $this->assertTrue($fixture->isTfaDisabled());

    // Setup, matching roles.
    $user_data->get('tfa', 3, 'tfa_user_settings')->willReturn([
@@ -218,7 +208,7 @@ class TfaContextTest extends UnitTestCase {
    $user->getRoles()->willReturn(['foo' => 'foo', 'bar' => 'bar']);
    $this->user = $user->reveal();
    $fixture = $this->getFixture();
    $this->assertTrue($fixture->isTfaRequired());
    $this->assertFalse($fixture->isTfaDisabled());
  }

  /**