You are here

class UserSyncEventSubscriber in SAML Authentication 4.x

Same name and namespace in other branches
  1. 8.3 src/EventSubscriber/UserSyncEventSubscriber.php \Drupal\samlauth\EventSubscriber\UserSyncEventSubscriber
  2. 8.2 src/EventSubscriber/UserSyncEventSubscriber.php \Drupal\samlauth\EventSubscriber\UserSyncEventSubscriber

Event subscriber that synchronizes user properties on a user_sync event.

This is basic module functionality, partially driven by config options. It's split out into an event subscriber so that the logic is easier to tweak for individual sites. (Set message or not? Completely break off login if an account with the same name is found, or continue with a non-renamed account? etc.)

Hierarchy

Expanded class hierarchy of UserSyncEventSubscriber

1 string reference to 'UserSyncEventSubscriber'
samlauth.services.yml in ./samlauth.services.yml
samlauth.services.yml
1 service uses UserSyncEventSubscriber
samlauth.event_subscriber.user_sync in ./samlauth.services.yml
Drupal\samlauth\EventSubscriber\UserSyncEventSubscriber

File

src/EventSubscriber/UserSyncEventSubscriber.php, line 29

Namespace

Drupal\samlauth\EventSubscriber
View source
class UserSyncEventSubscriber implements EventSubscriberInterface {
  use StringTranslationTrait;

  /**
   * The EntityTypeManager service.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The typed data manager.
   *
   * @var \Drupal\Core\TypedData\TypedDataManagerInterface
   */
  protected $typedDataManager;

  /**
   * The email validator.
   *
   * @var \Drupal\Component\Utility\EmailValidatorInterface
   */
  protected $emailValidator;

  /**
   * A logger instance.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected $logger;

  /**
   * A configuration object containing samlauth settings.
   *
   * @var \Drupal\Core\Config\ImmutableConfig
   */
  protected $config;

  /**
   * The messenger service.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected $messenger;

  /**
   * Constructs a new SamlauthUserSyncSubscriber.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The EntityTypeManager service.
   * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager
   *   The typed data manager.
   * @param \Drupal\Component\Utility\EmailValidatorInterface $email_validator
   *   The email validator. Note the code defines it as
   *   \Egulias\EmailValidator\EmailValidator for the time being; reason:
   *   - The default service used to be \Egulias\EmailValidator\EmailValidator,
   *     which in v1 only had one required argument. (v2 has two.)
   *   - From core 8.7, \Drupal\Component\Utility\EmailValidatorInterface was
   *     introduced, and the service now implements that interface AND still
   *     extends \Egulias\EmailValidator\EmailValidator, but makes the 2nd
   *     argument optional (and in fact, unusable) for backward compatibility.
   *   We already typehint the interface in comments, otherwise the call to
   *   isValid() will appear to contain errors. But we don't want to mandate
   *   Core >= 8.7 just yet, so the 'use' statement is still not updated.
   * @param \Psr\Log\LoggerInterface $logger
   *   A logger instance.
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The messenger service.
   * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
   *   The string translation service.
   */
  public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, TypedDataManagerInterface $typed_data_manager, EmailValidator $email_validator, LoggerInterface $logger, MessengerInterface $messenger, TranslationInterface $translation) {
    $this->entityTypeManager = $entity_type_manager;
    $this->emailValidator = $email_validator;
    $this->logger = $logger;
    $this->typedDataManager = $typed_data_manager;
    $this->config = $config_factory
      ->get('samlauth.authentication');
    $this
      ->setStringTranslation($translation);
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events[SamlauthEvents::USER_SYNC][] = [
      'onUserSync',
    ];
    return $events;
  }

  /**
   * Performs actions to synchronize users with SAML data on login.
   *
   * @param \Drupal\samlauth\Event\SamlauthUserSyncEvent $event
   *   The event.
   */
  public function onUserSync(SamlauthUserSyncEvent $event) {

    // If the account is new, we are in the middle of a user save operation;
    // the current user name is the authname as set by externalauth, and
    // e-mail is not set yet.
    $account = $event
      ->getAccount();
    $fatal_errors = [];

    // Synchronize username.
    // @todo in v4, can/should we get rid of most of this validation code, and
    //   just call $account->validate() afterwards? (Because that supposedly
    //   also checks for duplicate e-mail addresses etc.) This should be in
    //   'base' code, likely moved into externalauth if possible. (It's
    //   mentioned in #3132453. At the moment I think we should have an option
    //   for entity level validation, and keep field level validation in here.)
    if ($account
      ->isNew() || $this->config
      ->get('sync_name')) {

      // Get value from the SAML attribute whose name is configured in the
      // samlauth module.
      $name = $this
        ->getAttributeByConfig('user_name_attribute', $event);
      if ($name && $name != $account
        ->getAccountName()) {

        // Validate the username. This shouldn't be necessary to mitigate
        // attacks; assuming our SAML setup is correct, noone can insert fake
        // data here. It protects against SAML attribute misconfigurations.
        // Invalid names will cancel the login / account creation. The code is
        // copied from user_validate_name().
        $definition = BaseFieldDefinition::create('string')
          ->addConstraint('UserName', []);
        $data = $this->typedDataManager
          ->create($definition);
        $data
          ->setValue($name);
        $violations = $data
          ->validate();
        if ($violations) {
          foreach ($violations as $violation) {
            $fatal_errors[] = $violation
              ->getMessage();
          }
        }

        // Check if the username is not already taken by someone else. For new
        // accounts this can happen if the 'map existing users' setting is off.
        if (!$fatal_errors) {
          $account_search = $this->entityTypeManager
            ->getStorage('user')
            ->loadByProperties([
            'name' => $name,
          ]);
          $existing_account = reset($account_search);
          if (!$existing_account || $account
            ->id() == $existing_account
            ->id()) {
            $account
              ->setUsername($name);
            $event
              ->markAccountChanged();
          }
          else {
            $error = 'An account with the username @username already exists.';
            if ($account
              ->isNew()) {
              $fatal_errors[] = $this
                ->t($error, [
                '@username' => $name,
              ]);
            }
            else {

              // We continue and keep the old name. A DSM should be OK here
              // since login only happens interactively.
              $error = "Error updating user name from SAML attribute: {$error}";
              $this->logger
                ->error($error, [
                '@username' => $name,
              ]);
              $this->messenger
                ->addError($this
                ->t($error, [
                '@username' => $name,
              ]));
            }
          }
        }
      }
    }

    // Synchronize e-mail.
    if ($this->config
      ->get('user_mail_attribute') && ($account
      ->isNew() || $this->config
      ->get('sync_mail'))) {
      $mail = $this
        ->getAttributeByConfig('user_mail_attribute', $event);
      if ($mail) {
        if ($mail != $account
          ->getEmail()) {

          // Invalid e-mail cancels the login / account creation just like name.
          if ($this->emailValidator
            ->isValid($mail)) {
            $account
              ->setEmail($mail);
            if ($account
              ->isNew()) {

              // Externalauth sets init to a non e-mail value so we will fix it.
              $account
                ->set('init', $mail);
            }
            $event
              ->markAccountChanged();
          }
          else {
            $fatal_errors[] = $this
              ->t('Invalid e-mail address @mail', [
              '@mail' => $mail,
            ]);
          }
        }
      }
      elseif ($account
        ->isNew() && !$account
        ->getEmail()) {

        // We won't allow new accounts with empty e-mail. If a custom event
        // subscriber wants to populate the e-mail, then (at least for now) it
        // should be registered with a higher priority than this standard one.
        $fatal_errors[] = $this
          ->t('Email address is not provided in SAML attribute.');
      }
    }
    if ($fatal_errors) {

      // Cancel the whole login process and/or account creation.
      throw new \RuntimeException('Error(s) encountered during SAML attribute synchronization: ' . implode(' // ', $fatal_errors));
    }
  }

  /**
   * Returns value from a SAML attribute whose name is configured in our module.
   *
   * This is suitable for single-value attributes. (Most values are.)
   *
   * @param string $config_key
   *   A key in the module's configuration, containing the name of a SAML
   *   attribute.
   * @param \Drupal\samlauth\Event\SamlauthUserSyncEvent $event
   *   The event, which holds the attributes from the SAML response.
   *
   * @return mixed|null
   *   The SAML attribute value; NULL if the attribute value was not found.
   */
  public function getAttributeByConfig($config_key, SamlauthUserSyncEvent $event) {
    $attributes = $event
      ->getAttributes();
    $attribute_name = $this->config
      ->get($config_key);
    return $attribute_name && !empty($attributes[$attribute_name][0]) ? $attributes[$attribute_name][0] : NULL;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
StringTranslationTrait::$stringTranslation protected property The string translation service. 4
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.
UserSyncEventSubscriber::$config protected property A configuration object containing samlauth settings.
UserSyncEventSubscriber::$emailValidator protected property The email validator.
UserSyncEventSubscriber::$entityTypeManager protected property The EntityTypeManager service.
UserSyncEventSubscriber::$logger protected property A logger instance.
UserSyncEventSubscriber::$messenger protected property The messenger service.
UserSyncEventSubscriber::$typedDataManager protected property The typed data manager.
UserSyncEventSubscriber::getAttributeByConfig public function Returns value from a SAML attribute whose name is configured in our module.
UserSyncEventSubscriber::getSubscribedEvents public static function
UserSyncEventSubscriber::onUserSync public function Performs actions to synchronize users with SAML data on login.
UserSyncEventSubscriber::__construct public function Constructs a new SamlauthUserSyncSubscriber.