You are here

class UserSyncEventSubscriber in SAML Authentication 8.2

Same name and namespace in other branches
  1. 8.3 src/EventSubscriber/UserSyncEventSubscriber.php \Drupal\samlauth\EventSubscriber\UserSyncEventSubscriber
  2. 4.x 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

  • class \Drupal\samlauth\EventSubscriber\UserSyncEventSubscriber implements \Symfony\Component\EventDispatcher\EventSubscriberInterface

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 24

Namespace

Drupal\samlauth\EventSubscriber
View source
class UserSyncEventSubscriber implements EventSubscriberInterface {

  /**
   * 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 \Egulias\EmailValidator\EmailValidator
   */
  protected $emailValidator;

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

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

  /**
   * Construct 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 \Egulias\EmailValidator\EmailValidator $email_validator
   *   The email validator.
   * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager
   *   The typed data manager.
   * @param \Psr\Log\LoggerInterface $logger
   *   A logger instance.
   */
  public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, TypedDataManagerInterface $typed_data_manager, EmailValidator $email_validator, LoggerInterface $logger) {
    $this->entityTypeManager = $entity_type_manager;
    $this->emailValidator = $email_validator;
    $this->logger = $logger;
    $this->typedDataManager = $typed_data_manager;
    $this->config = $config_factory
      ->get('samlauth.authentication');
  }

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

  /**
   * Performs actions to synchronize users with Factory 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 'samlauth_AUTHNAME' (as set by externalauth) and
    // e-mail is not set yet.
    $account = $event
      ->getAccount();
    $fatal_errors = [];

    // Synchronize username.
    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 = \Drupal::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(array(
            '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[] = t($error, [
                '@username' => $name,
              ]);
            }
            else {

              // We continue and keep the old name. A DSM should be OK here
              // since login only happens interactively. (And we're ignoring
              // the law of dependency injection for this.)
              $error = "Error updating user name from SAML attribute: {$error}";
              $this->logger
                ->error($error, [
                '@username' => $name,
              ]);
              drupal_set_message(t($error, [
                '@username' => $name,
              ]), 'error');
            }
          }
        }
      }
    }

    // Synchronize e-mail.
    if ($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[] = t('Invalid e-mail address @mail', [
              '@mail' => $mail,
            ]);
          }
        }
      }
      elseif ($account
        ->isNew()) {

        // We won't allow new accounts with empty e-mail.
        $fatal_errors[] = 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: ' . join(' // ', $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
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::$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 Returns an array of event names this subscriber wants to listen to.
UserSyncEventSubscriber::onUserSync public function Performs actions to synchronize users with Factory data on login.
UserSyncEventSubscriber::__construct public function Construct a new SamlauthUserSyncSubscriber.