You are here

public function UserFieldsEventSubscriber::onUserSync in SAML Authentication 8.3

Same name and namespace in other branches
  1. 4.x modules/samlauth_user_fields/src/EventSubscriber/UserFieldsEventSubscriber.php \Drupal\samlauth_user_fields\EventSubscriber\UserFieldsEventSubscriber::onUserSync()

Saves configured SAML attribute values into user fields.

Parameters

\Drupal\samlauth\Event\SamlauthUserSyncEvent $event: The event being dispatched.

File

modules/samlauth_user_fields/src/EventSubscriber/UserFieldsEventSubscriber.php, line 229

Class

UserFieldsEventSubscriber
Synchronizes SAML attributes into user fields / links new users during login.

Namespace

Drupal\samlauth_user_fields\EventSubscriber

Code

public function onUserSync(SamlauthUserSyncEvent $event) {
  $account = $event
    ->getAccount();
  $config = $this->configFactory
    ->get(static::CONFIG_OBJECT_NAME);
  $mappings = $config
    ->get('field_mappings');
  $validation_errors = [];
  if (is_array($mappings)) {
    $compound_field_values = [];
    $changed_compound_field_values = [];
    foreach ($mappings as $mapping) {

      // If the attribute name is invalid, or the field does not exist, spam
      // the logs on every login until the mapping is fixed.
      if (empty($mapping['attribute_name']) || !is_string($mapping['attribute_name'])) {
        $this->logger
          ->warning('Invalid SAML attribute %attribute detected in mapping; the mapping must be fixed.');
      }
      elseif (empty($mapping['field_name']) || !is_string($mapping['field_name'])) {
        $this->logger
          ->warning('Invalid user field mapped from SAML attribute %attribute; the mapping must be fixed.', [
          '%attribute' => $mapping['attribute_name'],
        ]);
      }

      // Skip silently if the configured attribute is not present in our
      // data or if its value is considered 'empty / not updatable'.
      $value = $this
        ->getUpdatableAttributeValue($mapping['attribute_name'], $event
        ->getAttributes());
      if (isset($value)) {
        $account_field_name = strstr($mapping['field_name'], ':', TRUE);
        if ($account_field_name) {
          $sub_field_name = substr($mapping['field_name'], strlen($account_field_name) + 1);
        }
        else {
          $account_field_name = $mapping['field_name'];
          $sub_field_name = '';
        }
        $field_definition = $account
          ->getFieldDefinition($account_field_name);
        if (!$field_definition) {
          $this->logger
            ->warning('User field %field is mapped from SAML attribute %attribute, but does not exist; the mapping must be fixed.', [
            '%field' => $mapping['field_name'],
            '%attribute' => $mapping['attribute_name'],
          ]);
        }
        elseif ($sub_field_name && $field_definition
          ->getType() !== 'address') {

          // 'address' is the only compound field type we tested so far.
          $this->logger
            ->warning('Unsuppoted user field type %type; skipping field mapping.', [
            '%type' => $field_definition
              ->getType(),
          ]);
        }
        else {
          if (!$sub_field_name) {

            // Compare, validate, set single field.
            if (!$this
              ->isInputValueEqual($value, $account
              ->get($account_field_name)->value, $account_field_name)) {
              $valid = $this
                ->validateAccountFieldValue($value, $account, $mapping['field_name']);
              if ($valid) {
                $account
                  ->set($mapping['field_name'], $value);
                $event
                  ->markAccountChanged();
              }
              else {

                // Collect values to include below. Supposedly we have scalar
                // values; var_export() shows their type. And identifier
                // should include both source and destination because we can
                // have multiple mappings defined for either.
                $validation_errors[] = $mapping['attribute_name'] . ' (' . var_export($value, TRUE) . ') > ' . $mapping['field_name'];
              }
            }
          }
          else {

            // Get/compare compound field; if it should be updated, set the
            // changed field value aside for later validation, because
            // validation needs to be done on the field as a whole, and other
            // attributes may be mapped to other sub values.
            if (!isset($compound_field_values[$account_field_name])) {

              // TypedData: this only works with multivalue fields but I
              // guess that's a given anyway. We can either get() the
              // single value (specific object) or getValue() it, in which
              // case we assume it's an array, for our purpose. In the former
              // case, I guess
              // - typedDataManager->create($field_definition, $input_value)
              //   would get us a new value if our field is NULL (which can
              //   happen)
              // - validateAccountFieldValue() likely just works if we skip
              //   the create() call when $value is an object
              // but I haven't tried that. So far we just work with arrays.
              $compound_field_values[$account_field_name] = $account
                ->get($account_field_name)
                ->get(0)
                ->getValue() ?? [];
            }
            if (!$this
              ->isInputValueEqual($value, $compound_field_values[$account_field_name][$sub_field_name] ?? NULL, $mapping['field_name'])) {
              $compound_field_values[$account_field_name][$sub_field_name] = $value;

              // Just for logging if necessary:
              $changed_compound_field_values[$account_field_name][] = $mapping['attribute_name'] . ' (' . var_export($value, TRUE) . ')';
            }

            // This would be a step toward working with objects - untested:
            // TypedData uncertainty: get($sub_field_name) returns StringData
            // for address subfields; get($sub_field_name)->getValue()
            // returns the string. Both would be good for our current purpose
            // provided that isInputValueEqual() could handle classes.
            // if (!$this->isInputValueEqual($value, $account_field->get($sub_field_name)->getValue(), $mapping['field_name'])) {
            // $account_field->setValue($sub_field_name, $value);
            // $compound_field_values[$account_field_name] = $account_field;.
          }
        }
      }
    }
    if ($compound_field_values) {
      foreach ($compound_field_values as $field_name => $value) {
        $valid = $this
          ->validateAccountFieldValue($value, $account, $field_name);
        if ($valid) {
          $account
            ->set($field_name, $value);
          $event
            ->markAccountChanged();
        }
        else {
          $validation_errors[] = implode(' + ', $changed_compound_field_values) . " > {$field_name}";
        }
      }
    }
  }
  elseif (isset($mappings)) {
    $this->logger
      ->warning('Invalid %name configuration value; skipping user synchronization.', [
      '%name' => 'field_mappings',
    ]);
  }
  if ($validation_errors) {

    // Log an extra message summarizing which values failed validation,
    // because our field validation supposedly doesn't do that. The user is
    // expected to see the correlation between the different log messages.
    $this->logger
      ->warning('Validation errors were encountered while synchronizing SAML attributes into the user account: @values', [
      '@values' => implode(', ', $validation_errors),
    ]);
  }
}