You are here

ldap_user.module in Lightweight Directory Access Protocol (LDAP) 8.4

File

ldap_user/ldap_user.module
View source
<?php

/**
 * @file
 * Module for the LDAP User Entity.
 */
declare (strict_types=1);
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\ldap_servers\Helper\CredentialsStorage;
use Drupal\ldap_servers\LdapUserAttributesInterface;
use Drupal\ldap_user\Event\LdapNewUserCreatedEvent;
use Drupal\ldap_user\Event\LdapUserDeletedEvent;
use Drupal\ldap_user\Event\LdapUserUpdatedEvent;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Form\FormState;
use Drupal\Core\Field\FieldStorageDefinitionInterface;

/**
 * Implements hook_cron().
 */
function ldap_user_cron() {
  $check_orphans = \Drupal::config('ldap_user.settings')
    ->get('orphanedDrupalAcctBehavior');
  if ($check_orphans !== 'ldap_user_orphan_do_not_check') {

    /** @var \Drupal\ldap_user\Processor\OrphanProcessor $processor */
    $processor = \Drupal::service('ldap.orphan_processor');
    $processor
      ->checkOrphans();
  }
  $ldapUpdateQuery = \Drupal::config('ldap_user.settings')
    ->get('userUpdateCronQuery');
  if (\Drupal::moduleHandler()
    ->moduleExists('ldap_query') && $ldapUpdateQuery != NULL && $ldapUpdateQuery !== 'none') {

    /** @var \Drupal\ldap_user\Processor\GroupUserUpdateProcessor $processor */
    $processor = \Drupal::service('ldap.group_user_update_processor');
    if ($processor
      ->updateDue()) {
      $processor
        ->runQuery($ldapUpdateQuery);
    }
  }
}

/**
 * Implements hook_mail().
 */
function ldap_user_mail($key, &$message, $params) {
  switch ($key) {
    case 'orphaned_accounts':
      $message['subject'] = \Drupal::config('system.site')
        ->get('name') . ' ' . t('Orphaned LDAP Users');
      $message['body'][] = t('The following %count Drupal users no longer have corresponding LDAP entries. They probably have been removed from the directory and might need to be removed from your site.', [
        '%count' => count($params['accounts']),
      ]);
      $message['body'][] .= "\n" . t('Username,Mail,Link') . "\n" . implode("\n", $params['accounts']);
      break;
  }
}

/**
 * Implements hook_help().
 */
function ldap_user_help($route_name, RouteMatchInterface $route_match) {
  $ldap_user_help = t('LDAP user configuration determines how and when
     Drupal accounts are created based on LDAP data and which user fields
     are derived and synced to and from LDAP.');
  if ($route_name === 'help.page.ldap_user') {
    $output = '<h3>' . t('About') . '</h3>';
    $output .= '<p>' . $ldap_user_help . '</p>';
    return $output;
  }
}

/**
 * Implements hook_module_implements_alter().
 */
function ldap_user_module_implements_alter(&$implementations, $hook) {

  // We are moving authorization to the end because its user saving causes
  // issues.
  if ($hook === 'user_login' && isset($implementations['authorization'])) {
    $group = $implementations['authorization'];
    unset($implementations['authorization']);
    $implementations['authorization'] = $group;
  }
}

/**
 * Implements hook_user_login().
 */
function ldap_user_user_login($account) {

  /** @var \Drupal\ldap_user\Processor\DrupalUserProcessor $processor */
  $processor = \Drupal::service('ldap.drupal_user_processor');
  $processor
    ->drupalUserLogsIn($account);
}

/**
 * Implements hook_ENTITY_TYPE_insert().
 */
function ldap_user_user_insert($account) {
  $event = new LdapNewUserCreatedEvent($account);

  /** @var \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher */
  $dispatcher = \Drupal::service('event_dispatcher');
  if (version_compare(\Drupal::VERSION, '9.1', '>=')) {
    $dispatcher
      ->dispatch($event, LdapNewUserCreatedEvent::EVENT_NAME);
  }
  else {
    $dispatcher
      ->dispatch(LdapNewUserCreatedEvent::EVENT_NAME, $event);
  }
}

/**
 * Implements hook_ENTITY_TYPE_presave().
 */
function ldap_user_user_presave($account) {

  /** @var \Drupal\ldap_user\Processor\DrupalUserProcessor $processor */
  $processor = \Drupal::service('ldap.drupal_user_processor');
  if (!$account
    ->isNew()) {

    // We apply any data from LDAP to the Drupal user (if configured to do so)
    // before saving the user to avoid multiple saves on the entity.
    // We only do this after initial creation since we are otherwise potentially
    // querying for users that are set to be excluded on creation.
    $processor
      ->drupalUserUpdate($account);
  }
}

/**
 * Implements hook_ENTITY_TYPE_update().
 */
function ldap_user_user_update($account) {
  $event = new LdapUserUpdatedEvent($account);

  /** @var \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher */
  $dispatcher = \Drupal::service('event_dispatcher');
  if (version_compare(\Drupal::VERSION, '9.1', '>=')) {
    $dispatcher
      ->dispatch($event, LdapUserUpdatedEvent::EVENT_NAME);
  }
  else {
    $dispatcher
      ->dispatch(LdapUserUpdatedEvent::EVENT_NAME, $event);
  }
}

/**
 * Implements hook_ENTITY_TYPE_delete().
 */
function ldap_user_user_delete($account) {
  $event = new LdapUserDeletedEvent($account);

  /** @var \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher */
  $dispatcher = \Drupal::service('event_dispatcher');
  if (version_compare(\Drupal::VERSION, '9.1', '>=')) {
    $dispatcher
      ->dispatch($event, LdapUserDeletedEvent::EVENT_NAME);
  }
  else {
    $dispatcher
      ->dispatch(LdapUserDeletedEvent::EVENT_NAME, $event);
  }
}

/**
 * Implements hook_entity_base_field_info().
 */
function ldap_user_entity_base_field_info(EntityTypeInterface $entity_type) {
  if ($entity_type
    ->id() === 'user') {
    $fields = [];
    $fields['ldap_user_puid_sid'] = BaseFieldDefinition::create('string')
      ->setLabel(t('LDAP server ID'))
      ->setDescription(t('Server ID  that PUID was derived from. NULL if PUID is independent of server configuration instance.'));
    $fields['ldap_user_puid'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Permanent unique ID'))
      ->setDescription(t("The user's permanent unique ID should never change for a given LDAP identified user."));
    $fields['ldap_user_puid_property'] = BaseFieldDefinition::create('string')
      ->setLabel(t('PUID base property'))
      ->setDescription(t('The LDAP property used for the PUID, for example "dn".'));
    $fields['ldap_user_current_dn'] = BaseFieldDefinition::create('string')
      ->setLabel(t('LDAP DN'))
      ->setDescription(t("The user's LDAP DN. May change when user's DN changes."));
    $fields['ldap_user_prov_entries'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Provisioned LDAP entries'))
      ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
    $fields['ldap_user_last_checked'] = BaseFieldDefinition::create('timestamp')
      ->setLabel(t('Last LDAP comparison'))
      ->setDescription(t('Unix timestamp of when Drupal user was compared to LDAP entry. This could be for purposes of syncing, deleteing Drupal account, etc.'));
    $fields['ldap_user_ldap_exclude'] = BaseFieldDefinition::create('boolean')
      ->setLabel(t('Exclude from LDAP'))
      ->setDescription(t('Whether to exclude the user from LDAP functionality.'));
    return $fields;
  }
}

/* Below are form hooks which cannot be easily moved. */

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Relevant for user_login_block.
 */
function ldap_user_form_user_login_block_alter(&$form, &$form_state) {
  array_unshift($form['#validate'], 'ldap_user_grab_password_validate');
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Relevant for some contrib modules such as prlp, which add a password
 * field into the password reset page.
 */
function ldap_user_form_user_pass_reset_alter(&$form, $form_state) {
  array_unshift($form['#validate'], 'ldap_user_grab_password_validate');
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Relevant for user_login_form.
 */
function ldap_user_form_user_login_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  array_unshift($form['#validate'], 'ldap_user_grab_password_validate');
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Relevant for user profile form.
 */
function ldap_user_form_user_form_alter(&$form, $form_state) {
  array_unshift($form['#validate'], 'ldap_user_grab_password_validate');
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Relevant for password_policy_password_tab.
 */
function ldap_user_form_password_policy_password_tab_alter(&$form, &$form_state) {
  array_unshift($form['#validate'], 'ldap_user_grab_password_validate');
}

/**
 * Alter password form through validation.
 *
 * Store password from logon forms in ldap_user_ldap_provision_pwd static
 * variable for use in provisioning to LDAP.
 */
function ldap_user_grab_password_validate($form, FormState $form_state) {

  // This is not a login form but profile form and user is inserting password
  // to update email.
  if (!empty($form_state
    ->getValue('current_pass_required_values'))) {
    if (!empty($form_state
      ->getValue('current_pass')) && empty($form_state
      ->getValue('pass'))) {
      CredentialsStorage::storeUserPassword($form_state
        ->getValue('current_pass'));
    }
  }
  elseif (!empty($form_state
    ->getValue('pass'))) {
    CredentialsStorage::storeUserPassword($form_state
      ->getValue('pass'));
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * For user_register_form.
 */
function ldap_user_form_user_register_form_alter(&$form, $form_state) {
  $user_settings = \Drupal::config('ldap_user.settings');
  array_unshift($form['#submit'], 'ldap_user_grab_password_validate');
  if (!\Drupal::currentUser()
    ->hasPermission('administer users')) {
    return;
  }
  if ($user_settings
    ->get('disableAdminPasswordField') == TRUE) {
    $form['account']['pass']['#type'] = 'value';
    if (version_compare(\Drupal::VERSION, '9.1', '>=')) {
      $form['account']['pass']['#value'] = \Drupal::service('password_generator')
        ->generate(40);
    }
    else {
      $form['account']['pass']['#value'] = user_password(40);
    }
    $form['account']['pass_disabled']['#type'] = 'fieldset';
    $form['account']['pass_disabled']['#title'] = t('Password');
    $form['account']['pass_disabled'][]['#markup'] = t('LDAP has disabled the password field and generated a random password.');
  }
  $form['ldap_user_fields']['#type'] = 'fieldset';
  $form['ldap_user_fields']['#title'] = t('LDAP Options');
  $form['ldap_user_fields']['#description'] = t('By enabling options in the LDAP user configuration, you can allow the creation of LDAP accounts and define the conflict resolution for associated accounts.');
  $form['ldap_user_fields']['#collapsible'] = TRUE;
  $form['ldap_user_fields']['#collapsed'] = FALSE;
  $form['ldap_user_fields']['ldap_user_association'] = [
    '#type' => 'radios',
    '#options' => [
      LdapUserAttributesInterface::MANUAL_ACCOUNT_CONFLICT_LDAP_ASSOCIATE => t('Associate account'),
      LdapUserAttributesInterface::MANUAL_ACCOUNT_CONFLICT_NO_LDAP_ASSOCIATE => t('Do not associated account'),
    ],
    '#description' => t('If you choose associated account and an LDAP account cannot be found, a validation error will appear and the account will not be created.'),
    '#title' => t('LDAP Entry Association.'),
  ];
  if ($user_settings
    ->get('ldapEntryProvisionTriggers') && in_array(LdapUserAttributesInterface::PROVISION_DRUPAL_USER_ON_USER_UPDATE_CREATE, $user_settings
    ->get('ldapEntryProvisionTriggers'), TRUE)) {
    $form['ldap_user_fields']['ldap_user_association']['#access'] = FALSE;
  }
  elseif ($user_settings
    ->get('manualAccountConflict') !== LdapUserAttributesInterface::MANUAL_ACCOUNT_CONFLICT_SHOW_OPTION_ON_FORM) {
    $form['ldap_user_fields']['ldap_user_association']['#access'] = FALSE;
  }
  else {
    $form['ldap_user_fields']['ldap_user_association']['#default_value'] = LdapUserAttributesInterface::MANUAL_ACCOUNT_CONFLICT_LDAP_ASSOCIATE;
  }
  $form['ldap_user_fields']['ldap_user_create_ldap_acct'] = [
    '#type' => 'checkbox',
    '#title' => t('Create corresponding LDAP entry.'),
  ];
  if (!in_array(LdapUserAttributesInterface::PROVISION_DRUPAL_USER_ON_USER_ON_MANUAL_CREATION, $user_settings
    ->get('ldapEntryProvisionTriggers'), TRUE)) {
    $form['ldap_user_fields']['ldap_user_create_ldap_acct']['#access'] = FALSE;
  }
  $form['#validate'][] = 'ldap_user_form_register_form_validate';
  foreach (array_keys($form['actions']) as $action) {
    if (isset($form['actions'][$action]['#type']) && $form['actions'][$action]['#type'] === 'submit') {
      $form['actions'][$action]['#submit'][] = 'ldap_user_form_register_form_submit2';
    }
  }
}

/**
 * Implements hook_form_validate().
 */
function ldap_user_form_register_form_validate($form, FormStateInterface $form_state) {
  $config = \Drupal::config('ldap_user.settings');

  /** @var \Drupal\ldap_servers\LdapUserManager $ldap_user_manager */
  $ldap_user_manager = \Drupal::service('ldap.user_manager');
  if (empty($form_state
    ->getValue('ldap_user_association'))) {
    $form_state
      ->setValue('ldap_user_association', $config
      ->get('manualAccountConflict'));
  }
  if ($form_state
    ->getValue('ldap_user_association') === LdapUserAttributesInterface::MANUAL_ACCOUNT_CONFLICT_NO_LDAP_ASSOCIATE) {
    $form_state
      ->set('ldap_user_ldap_exclude', 1);
  }

  // If the corresponding LDAP account does not exist and provision not
  // selected and make LDAP associated is selected, throw error.
  if (!$form_state
    ->getValue('ldap_user_create_ldap_acct') && $form_state
    ->getValue('ldap_user_association') === LdapUserAttributesInterface::MANUAL_ACCOUNT_CONFLICT_LDAP_ASSOCIATE && empty($config
    ->get('drupalAcctProvisionServer'))) {
    $form_state
      ->setErrorByName('ldap_user_missing_', t('The provisioning server is not set up correctly.'));
    \Drupal::logger('ldap_user')
      ->error('No server available for provisioning to Drupal.');
  }

  // If trying to provision an LDAP account and one already exists, throw error.
  if ($form_state
    ->getValue('ldap_user_create_ldap_acct')) {
    if (empty($config
      ->get('ldapEntryProvisionServer'))) {
      $form_state
        ->setErrorByName('ldap_user_missing_', t('The provisioning server is not set up correctly.'));
      \Drupal::logger('ldap_user')
        ->error('No server available for provisioning to LDAP.');
    }
    else {
      $ldap_user_manager
        ->setServerById($config
        ->get('ldapEntryProvisionServer'));
      $ldap_user = $ldap_user_manager
        ->getUserDataByIdentifier($form_state
        ->getValue('name'));
      if ($ldap_user) {
        $form_state
          ->setErrorByName('ldap_user_create_ldap_acct', t('User %name already has a corresponding LDAP Entry (%dn). Uncheck "Create corresponding LDAP entry" to allow this Drupal user to be created. Select "Make this an LDAP associated account" to associate this account with the LDAP entry.', [
          '%dn' => $ldap_user
            ->getDn(),
          '%name' => $form_state
            ->getValue('name'),
        ]));
      }
    }
  }

  // If a conflict with an LDAP account exists (no association), throw error.
  if ($form_state
    ->getValue('ldap_user_association') === LdapUserAttributesInterface::MANUAL_ACCOUNT_CONFLICT_REJECT) {

    // @todo The behavior of what to do with missing provisioning server in the
    //   validation check cases is mostly undefined. Ideally we'd prevent such
    //   a setup from occurring, or at least behaving more consistently.
    if ($config
      ->get('drupalAcctProvisionServer')) {
      $ldap_user_manager
        ->setServerById($config
        ->get('drupalAcctProvisionServer'));
      $ldap_user = $ldap_user_manager
        ->getUserDataByIdentifier($form_state
        ->getValue('name'));
      if ($ldap_user) {
        $form_state
          ->setErrorByName('name', t('User %name conflicts with an LDAP Entry (%dn). Creation blocked per your configuration.', [
          '%dn' => $ldap_user
            ->getDn(),
          '%name' => $form_state
            ->getValue('name'),
        ]));
      }
    }
    else {
      \Drupal::logger('ldap_user')
        ->notice('No server available for provisioning to Drupal, conflict rejection has no effect.');
    }
  }
}

/**
 * Called after user_register_form_submit.
 */
function ldap_user_form_register_form_submit2(&$form, FormState $form_state) {

  // It's only called when a user who can create a new user does so using the
  // register form.
  $values = $form_state
    ->getValues();

  /** @var \Drupal\ldap_user\Processor\DrupalUserProcessor $userProcessor */
  $userProcessor = \Drupal::service('ldap.drupal_user_processor');
  if ($values['ldap_user_association'] === LdapUserAttributesInterface::MANUAL_ACCOUNT_CONFLICT_NO_LDAP_ASSOCIATE) {
    $userProcessor
      ->ldapExcludeDrupalAccount($values['name']);
  }
  elseif ($values['ldap_user_association'] === LdapUserAttributesInterface::MANUAL_ACCOUNT_CONFLICT_LDAP_ASSOCIATE) {

    // Either LDAP provision (above) has said "associate" or the person creating
    // the account has said "associate" or the LDAP user settings says
    // "Associate manually created Drupal accounts with related LDAP Account
    // if one exists.".
    $association = $userProcessor
      ->ldapAssociateDrupalAccount($values['name']);
    if (!$association) {
      \Drupal::messenger()
        ->addWarning(t('Account created but no LDAP account found to associate with.'));
    }
  }
}

/**
 * Implements hook_entity_base_field_info_alter().
 */
function ldap_user_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) {
  if ($entity_type
    ->id() !== 'user') {
    return;
  }
  $field_names = [
    'pass',
    'mail',
  ];
  foreach ($field_names as $field_name) {

    /** @var BaseFieldDefinition $field */
    $field = $fields[$field_name];
    $constraints = $field
      ->getConstraints();
    $new_constraints = [];

    // Replaces the core constraint on user fields with an LDAP-specific one
    // to allow for updating mail and pass.
    $changed = FALSE;
    foreach ($constraints as $name => $options) {
      if ($name === 'ProtectedUserField') {
        $name = 'LdapProtectedUserField';
        $changed = TRUE;
      }
      $new_constraints[$name] = $options;
    }
    if ($changed) {
      $field
        ->setConstraints($new_constraints);
    }
  }
}