[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/administrator/components/com_users/src/Helper/ -> Mfa.php (source)

   1  <?php
   2  
   3  /**
   4   * @package        Joomla.Administrator
   5   * @subpackage     com_users
   6   *
   7   * @copyright  (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
   8   * @license        GNU General Public License version 2 or later; see LICENSE.txt
   9   */
  10  
  11  namespace Joomla\Component\Users\Administrator\Helper;
  12  
  13  use Exception;
  14  use Joomla\CMS\Application\CMSApplication;
  15  use Joomla\CMS\Component\ComponentHelper;
  16  use Joomla\CMS\Document\HtmlDocument;
  17  use Joomla\CMS\Event\MultiFactor\GetMethod;
  18  use Joomla\CMS\Factory;
  19  use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
  20  use Joomla\CMS\Plugin\PluginHelper;
  21  use Joomla\CMS\Uri\Uri;
  22  use Joomla\CMS\User\User;
  23  use Joomla\CMS\User\UserFactoryInterface;
  24  use Joomla\Component\Users\Administrator\DataShape\MethodDescriptor;
  25  use Joomla\Component\Users\Administrator\Model\BackupcodesModel;
  26  use Joomla\Component\Users\Administrator\Model\MethodsModel;
  27  use Joomla\Component\Users\Administrator\Table\MfaTable;
  28  use Joomla\Component\Users\Administrator\View\Methods\HtmlView;
  29  use Joomla\Database\DatabaseDriver;
  30  use Joomla\Database\ParameterType;
  31  
  32  // phpcs:disable PSR1.Files.SideEffects
  33  \defined('_JEXEC') or die;
  34  // phpcs:enable PSR1.Files.SideEffects
  35  
  36  /**
  37   * Helper functions for captive MFA handling
  38   *
  39   * @since 4.2.0
  40   */
  41  abstract class Mfa
  42  {
  43      /**
  44       * Cache of all currently active MFAs
  45       *
  46       * @var   array|null
  47       * @since 4.2.0
  48       */
  49      protected static $allMFAs = null;
  50  
  51      /**
  52       * Are we inside the administrator application
  53       *
  54       * @var   boolean
  55       * @since 4.2.0
  56       */
  57      protected static $isAdmin = null;
  58  
  59      /**
  60       * Get the HTML for the Multi-factor Authentication configuration interface for a user.
  61       *
  62       * This helper method uses a sort of primitive HMVC to display the com_users' Methods page which
  63       * renders the MFA configuration interface.
  64       *
  65       * @param   User  $user  The user we are going to show the configuration UI for.
  66       *
  67       * @return  string|null  The HTML of the UI; null if we cannot / must not show it.
  68       * @throws  Exception
  69       * @since   4.2.0
  70       */
  71      public static function getConfigurationInterface(User $user): ?string
  72      {
  73          // Check the conditions
  74          if (!self::canShowConfigurationInterface($user)) {
  75              return null;
  76          }
  77  
  78          /** @var CMSApplication $app */
  79          $app = Factory::getApplication();
  80  
  81          if (!$app->input->getCmd('option', '') === 'com_users') {
  82              $app->getLanguage()->load('com_users');
  83              $app->getDocument()
  84                  ->getWebAssetManager()
  85                  ->getRegistry()
  86                  ->addExtensionRegistryFile('com_users');
  87          }
  88  
  89          // Get a model
  90          /** @var MVCFactoryInterface $factory */
  91          $factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory();
  92  
  93          /** @var MethodsModel $methodsModel */
  94          $methodsModel = $factory->createModel('Methods', 'Administrator');
  95          /** @var BackupcodesModel $methodsModel */
  96          $backupCodesModel = $factory->createModel('Backupcodes', 'Administrator');
  97  
  98          // Get a view object
  99          $appRoot = $app->isClient('site') ? \JPATH_SITE : \JPATH_ADMINISTRATOR;
 100          $prefix  = $app->isClient('site') ? 'Site' : 'Administrator';
 101          /** @var HtmlView $view */
 102          $view = $factory->createView(
 103              'Methods',
 104              $prefix,
 105              'Html',
 106              [
 107                  'base_path' => $appRoot . '/components/com_users',
 108              ]
 109          );
 110          $view->setModel($methodsModel, true);
 111          /** @noinspection PhpParamsInspection */
 112          $view->setModel($backupCodesModel);
 113          $view->document  = $app->getDocument();
 114          $view->returnURL = base64_encode(Uri::getInstance()->toString());
 115          $view->user      = $user;
 116          $view->set('forHMVC', true);
 117  
 118          @ob_start();
 119  
 120          try {
 121              $view->display();
 122          } catch (\Throwable $e) {
 123              @ob_end_clean();
 124  
 125              /**
 126               * This is intentional! When you are developing a Multi-factor Authentication plugin you
 127               * will inevitably mess something up and end up with an error. This would cause the
 128               * entire MFA configuration page to disappear. No problem! Set Debug System to Yes in
 129               * Global Configuration and you can see the error exception which will help you solve
 130               * your problem.
 131               */
 132              if (defined('JDEBUG') && JDEBUG) {
 133                  throw $e;
 134              }
 135  
 136              return null;
 137          }
 138  
 139          return @ob_get_clean();
 140      }
 141  
 142      /**
 143       * Get a list of all of the MFA Methods
 144       *
 145       * @return  MethodDescriptor[]
 146       * @since 4.2.0
 147       */
 148      public static function getMfaMethods(): array
 149      {
 150          PluginHelper::importPlugin('multifactorauth');
 151  
 152          if (is_null(self::$allMFAs)) {
 153              // Get all the plugin results
 154              $event = new GetMethod();
 155              $temp  = Factory::getApplication()
 156                  ->getDispatcher()
 157                  ->dispatch($event->getName(), $event)
 158                  ->getArgument('result', []);
 159  
 160              // Normalize the results
 161              self::$allMFAs = [];
 162  
 163              foreach ($temp as $method) {
 164                  if (!is_array($method) && !($method instanceof MethodDescriptor)) {
 165                      continue;
 166                  }
 167  
 168                  $method = $method instanceof MethodDescriptor
 169                      ? $method : new MethodDescriptor($method);
 170  
 171                  if (empty($method['name'])) {
 172                      continue;
 173                  }
 174  
 175                  self::$allMFAs[$method['name']] = $method;
 176              }
 177          }
 178  
 179          return self::$allMFAs;
 180      }
 181  
 182      /**
 183       * Is the current user allowed to add/edit MFA methods for $user?
 184       *
 185       * This is only allowed if I am adding / editing methods for myself.
 186       *
 187       * If the target user is a member of any group disallowed to use MFA this will return false.
 188       *
 189       * @param   User|null  $user  The user you want to know if we're allowed to edit
 190       *
 191       * @return  boolean
 192       * @throws  Exception
 193       * @since 4.2.0
 194       */
 195      public static function canAddEditMethod(?User $user = null): bool
 196      {
 197          // Cannot do MFA operations on no user or a guest user.
 198          if (is_null($user) || $user->guest) {
 199              return false;
 200          }
 201  
 202          // If the user is in a user group which disallows MFA we cannot allow adding / editing methods.
 203          $neverMFAGroups = ComponentHelper::getParams('com_users')->get('neverMFAUserGroups', []);
 204          $neverMFAGroups = is_array($neverMFAGroups) ? $neverMFAGroups : [];
 205  
 206          if (count(array_intersect($user->getAuthorisedGroups(), $neverMFAGroups))) {
 207              return false;
 208          }
 209  
 210          // Check if this is the same as the logged-in user.
 211          $myUser = Factory::getApplication()->getIdentity()
 212              ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0);
 213  
 214          return $myUser->id === $user->id;
 215      }
 216  
 217      /**
 218       * Is the current user allowed to delete MFA methods / disable MFA for $user?
 219       *
 220       * This is allowed if:
 221       * - The user being queried is the same as the logged-in user
 222       * - The logged-in user is a Super User AND the queried user is NOT a Super User.
 223       *
 224       * Note that Super Users can be edited by their own user only for security reasons. If a Super
 225       * User gets locked out they must use the Backup Codes to regain access. If that's not possible,
 226       * they will need to delete their records from the `#__user_mfa` table.
 227       *
 228       * @param   User|null  $user  The user being queried.
 229       *
 230       * @return  boolean
 231       * @throws  Exception
 232       * @since   4.2.0
 233       */
 234      public static function canDeleteMethod(?User $user = null): bool
 235      {
 236          // Cannot do MFA operations on no user or a guest user.
 237          if (is_null($user) || $user->guest) {
 238              return false;
 239          }
 240  
 241          $myUser = Factory::getApplication()->getIdentity()
 242              ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0);
 243  
 244          return $myUser->id === $user->id
 245              || ($myUser->authorise('core.admin') && !$user->authorise('core.admin'));
 246      }
 247  
 248      /**
 249       * Return all MFA records for a specific user
 250       *
 251       * @param   int|null  $userId  User ID. NULL for currently logged in user.
 252       *
 253       * @return  MfaTable[]
 254       * @throws  Exception
 255       *
 256       * @since 4.2.0
 257       */
 258      public static function getUserMfaRecords(?int $userId): array
 259      {
 260          if (empty($userId)) {
 261              $user   = Factory::getApplication()->getIdentity() ?: Factory::getUser();
 262              $userId = $user->id ?: 0;
 263          }
 264  
 265          /** @var DatabaseDriver $db */
 266          $db    = Factory::getContainer()->get('DatabaseDriver');
 267          $query = $db->getQuery(true)
 268              ->select($db->quoteName('id'))
 269              ->from($db->quoteName('#__user_mfa'))
 270              ->where($db->quoteName('user_id') . ' = :user_id')
 271              ->bind(':user_id', $userId, ParameterType::INTEGER);
 272  
 273          try {
 274              $ids = $db->setQuery($query)->loadColumn() ?: [];
 275          } catch (Exception $e) {
 276              $ids = [];
 277          }
 278  
 279          if (empty($ids)) {
 280              return [];
 281          }
 282  
 283          /** @var MVCFactoryInterface $factory */
 284          $factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory();
 285  
 286          // Map all results to MFA table objects
 287          $records = array_map(
 288              function ($id) use ($factory) {
 289                  /** @var MfaTable $record */
 290                  $record = $factory->createTable('Mfa', 'Administrator');
 291                  $loaded = $record->load($id);
 292  
 293                  return $loaded ? $record : null;
 294              },
 295              $ids
 296          );
 297  
 298          // Let's remove Methods we couldn't decrypt when reading from the database.
 299          $hasBackupCodes = false;
 300  
 301          $records = array_filter(
 302              $records,
 303              function ($record) use (&$hasBackupCodes) {
 304                  $isValid = !is_null($record) && (!empty($record->options));
 305  
 306                  if ($isValid && ($record->method === 'backupcodes')) {
 307                      $hasBackupCodes = true;
 308                  }
 309  
 310                  return $isValid;
 311              }
 312          );
 313  
 314          // If the only Method is backup codes it's as good as having no records
 315          if ((count($records) === 1) && $hasBackupCodes) {
 316              return [];
 317          }
 318  
 319          return $records;
 320      }
 321  
 322      /**
 323       * Are the conditions for showing the MFA configuration interface met?
 324       *
 325       * @param   User|null  $user  The user to be configured
 326       *
 327       * @return  boolean
 328       * @throws  Exception
 329       * @since 4.2.0
 330       */
 331      public static function canShowConfigurationInterface(?User $user = null): bool
 332      {
 333          // If I have no user to check against that's all the checking I can do.
 334          if (empty($user)) {
 335              return false;
 336          }
 337  
 338          // I need at least one MFA method plugin for the setup interface to make any sense.
 339          $plugins = PluginHelper::getPlugin('multifactorauth');
 340  
 341          if (count($plugins) < 1) {
 342              return false;
 343          }
 344  
 345          /** @var CMSApplication $app */
 346          $app = Factory::getApplication();
 347  
 348          // We can only show a configuration page in the front- or backend application.
 349          if (!$app->isClient('site') && !$app->isClient('administrator')) {
 350              return false;
 351          }
 352  
 353          // Only show the configuration page if we have an HTML document
 354          if (!($app->getDocument() instanceof HtmlDocument)) {
 355              return false;
 356          }
 357  
 358          // I must be able to add, edit or delete the user's MFA settings
 359          return self::canAddEditMethod($user) || self::canDeleteMethod($user);
 360      }
 361  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer