You are here

abstract class LdapBaseManager in Lightweight Directory Access Protocol (LDAP) 8.4

LDAP Base Manager.

Hierarchy

Expanded class hierarchy of LdapBaseManager

File

ldap_servers/src/LdapBaseManager.php, line 17

Namespace

Drupal\ldap_servers
View source
abstract class LdapBaseManager {
  use LdapTransformationTraits;

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

  /**
   * Entity Type Manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * LDAP Bridge.
   *
   * @var \Drupal\ldap_servers\LdapBridge
   */
  protected $ldapBridge;

  /**
   * Module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandler
   */
  protected $moduleHandler;

  /**
   * Symfony Ldap.
   *
   * @var \Symfony\Component\Ldap\Ldap
   */
  protected $ldap;

  /**
   * Server.
   *
   * @var \Drupal\ldap_servers\Entity\Server
   */
  protected $server;

  /**
   * Constructor.
   *
   * @param \Psr\Log\LoggerInterface $logger
   *   Logger.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   Entity type manager.
   * @param \Drupal\ldap_servers\LdapBridgeInterface $ldap_bridge
   *   LDAP Bridge.
   * @param \Drupal\Core\Extension\ModuleHandler $module_handler
   *   Module handler.
   */
  public function __construct(LoggerInterface $logger, EntityTypeManagerInterface $entity_type_manager, LdapBridgeInterface $ldap_bridge, ModuleHandler $module_handler) {
    $this->logger = $logger;
    $this->entityTypeManager = $entity_type_manager;
    $this->ldapBridge = $ldap_bridge;
    $this->moduleHandler = $module_handler;
  }

  /**
   * Set server by ID.
   *
   * @param string $sid
   *   Server machine name.
   *
   * @return bool
   *   Binding successful.
   */
  public function setServerById(string $sid) : bool {

    /** @var \Drupal\ldap_servers\Entity\Server $server */
    $server = $this->entityTypeManager
      ->getStorage('ldap_server')
      ->load($sid);
    return $this
      ->setServer($server);
  }

  /**
   * Set server by ID.
   *
   * @param \Drupal\ldap_servers\Entity\Server $server
   *   LDAP Server.
   *
   * @return bool
   *   Binding successful.
   */
  public function setServer(Server $server) : bool {
    $this->server = $server;
    $this->ldapBridge
      ->setServer($this->server);
    $bind_result = $this->ldapBridge
      ->bind();
    $this->ldap = $this->ldapBridge
      ->get();
    return $bind_result;
  }

  /**
   * Check availability of service.
   *
   * We have to explicitly check this in many calls since the Server might not
   * have been set yet.
   */
  protected function checkAvailability() : bool {
    if ($this->server && $this->ldapBridge
      ->bind()) {
      return TRUE;
    }
    $this->logger
      ->error("LDAP server unavailable");
    return FALSE;
  }

  /**
   * Does dn exist for this server and what is its data?
   *
   * @param string $dn
   *   DN to search for.
   * @param array $attributes
   *   In same form as ldap_read $attributes parameter.
   *
   * @return bool|Entry
   *   Return ldap entry or false.
   *
   * @todo Entry/null would be better for type hints.
   */
  public function checkDnExistsIncludeData(string $dn, array $attributes) {
    if (!$this
      ->checkAvailability()) {
      return FALSE;
    }
    $options = [
      'filter' => $attributes,
      'scope' => 'base',
    ];
    try {
      $result = $this->ldap
        ->query($dn, '(objectclass=*)', $options)
        ->execute();
    } catch (LdapException $e) {
      return FALSE;
    }
    if ($result
      ->count() > 0) {
      return $result
        ->toArray()[0];
    }
    return FALSE;
  }

  /**
   * Does dn exist for this server?
   *
   * @param string $dn
   *   DN to search for.
   *
   * @return bool
   *   DN exists.
   */
  public function checkDnExists(string $dn) : bool {
    if (!$this
      ->checkAvailability()) {
      return FALSE;
    }
    $options = [
      'filter' => [
        'objectclass',
      ],
      'scope' => 'base',
    ];
    try {
      $result = $this->ldap
        ->query($dn, '(objectclass=*)', $options)
        ->execute();
    } catch (LdapException $e) {
      return FALSE;
    }
    return $result
      ->count() > 0;
  }

  /**
   * Perform an LDAP search on all base dns and aggregate into one result.
   *
   * @param string $filter
   *   The search filter, such as sAMAccountName=jbarclay. Attribute values
   *   (e.g. jbarclay) should be esacaped before calling.
   * @param array $attributes
   *   List of desired attributes. If omitted, we only return "dn".
   *
   * @return \Symfony\Component\Ldap\Entry[]
   *   An array of matching entries combined from all DN.
   */
  public function searchAllBaseDns(string $filter, array $attributes = []) : array {
    $all_entries = [];
    if (!$this
      ->checkAvailability()) {
      return $all_entries;
    }
    $options = [
      'filter' => $attributes,
    ];
    $results = [];
    foreach ($this->server
      ->getBaseDn() as $base_dn) {
      $relative_filter = str_replace(',' . $base_dn, '', $filter);
      try {
        $ldap_response = $this->ldap
          ->query($base_dn, $relative_filter, $options)
          ->execute();
      } catch (LdapException $e) {
        $this->logger
          ->critical('LDAP search error with @message', [
          '@message' => $e
            ->getMessage(),
        ]);
        continue;
      }
      if ($ldap_response
        ->count() > 0) {
        $results[] = $ldap_response
          ->toArray();
      }
    }
    return array_merge(...$results);
  }

  /**
   * Create LDAP entry.
   *
   * @param \Symfony\Component\Ldap\Entry $entry
   *   Entry.
   *
   * @return bool
   *   Result of action.
   */
  public function createLdapEntry(Entry $entry) : bool {
    if (!$this
      ->checkAvailability()) {
      return FALSE;
    }
    try {
      $this->ldap
        ->getEntryManager()
        ->add($entry);
    } catch (LdapException $e) {
      $this->logger
        ->error("LDAP server @sid exception: %ldap_error", [
        '@sid' => $this->server
          ->id(),
        '%ldap_error' => $e
          ->getMessage(),
      ]);
      return FALSE;
    }
    return TRUE;
  }

  /**
   * Modify attributes of LDAP entry.
   *
   * @param \Symfony\Component\Ldap\Entry $entry
   *   LDAP entry.
   *
   * @return bool
   *   Result of query.
   */
  public function modifyLdapEntry(Entry $entry) : bool {
    if (!$this
      ->checkAvailability()) {
      return FALSE;
    }

    // @todo Verify unicodePwd was modified if present through alter hook.
    try {
      $this->ldap
        ->getEntryManager()
        ->update($entry);
    } catch (LdapException $e) {
      $this->logger
        ->error("LDAP server error updating %dn on @sid exception: %ldap_error", [
        '%dn' => $entry
          ->getDn(),
        '@sid' => $this->server
          ->id(),
        '%ldap_error' => $e
          ->getMessage(),
      ]);
      return FALSE;
    }
    return TRUE;
  }

  /**
   * Perform an LDAP delete.
   *
   * @param string $dn
   *   DN of entry.
   *
   * @return bool
   *   Result of ldap_delete() call.
   */
  public function deleteLdapEntry(string $dn) : bool {
    if (!$this
      ->checkAvailability()) {
      return FALSE;
    }
    try {
      $this->ldap
        ->getEntryManager()
        ->remove(new Entry($dn));
    } catch (LdapException $e) {
      $this->logger
        ->error("LDAP entry '%dn' could not be delete from from server @sid: @message", [
        '%dn' => $dn,
        '@sid' => $this->server
          ->id(),
        '@message' => $e
          ->getMessage(),
      ]);
      return FALSE;
    }
    $this->logger
      ->info("LDAP entry '%dn' deleted from server @sid", [
      '%dn' => $dn,
      '@sid' => $this->server
        ->id(),
    ]);
    return TRUE;
  }

  /**
   * Apply modifications to entry.
   *
   * @param \Symfony\Component\Ldap\Entry $entry
   *   Entry.
   * @param \Symfony\Component\Ldap\Entry $current
   *   Current.
   */
  protected function applyModificationsToEntry(Entry $entry, Entry $current) : void {

    // @todo Make sure the empty attributes sent are actually an array.
    // @todo Make sure that count et al are gone.
    foreach ($entry
      ->getAttributes() as $new_key => $new_value) {
      if ($current
        ->getAttribute($new_key, FALSE) == $new_value) {
        $entry
          ->removeAttribute($new_key);
      }
    }
  }

  /**
   * Match username to existing LDAP entry.
   *
   * @param string $drupal_username
   *   Drupal username.
   *
   * @return false|null|\Symfony\Component\Ldap\Entry
   *   LDAP Entry.
   *
   * @see \Drupal\ldap_servers\LdapUserManager::getUserDataByIdentifier
   */
  public function matchUsernameToExistingLdapEntry(string $drupal_username) {
    $result = $this
      ->queryAllBaseDnLdapForUsername($drupal_username);
    if ($result !== FALSE) {
      $result = $this
        ->sanitizeUserDataResponse($result, $drupal_username);
    }
    return $result;
  }

  /**
   * Queries LDAP server for the user.
   *
   * @param string $drupal_username
   *   Drupal user name.
   *
   * @return \Symfony\Component\Ldap\Entry|false
   *   LDAP Entry.
   *
   * @todo This function does return data and check for validity of response.
   *  This makes responses difficult to parse and should be optimized.
   */
  public function queryAllBaseDnLdapForUsername(string $drupal_username) {
    if (!$this
      ->checkAvailability()) {
      return FALSE;
    }
    foreach ($this->server
      ->getBaseDn() as $base_dn) {
      $result = $this
        ->queryLdapForUsername($base_dn, $drupal_username);
      if ($result === FALSE || $result instanceof Entry) {
        return $result;
      }
    }
    return FALSE;
  }

  /**
   * Sanitize user data response.
   *
   * @param \Symfony\Component\Ldap\Entry $entry
   *   LDAP entry.
   * @param string $drupal_username
   *   Drupal username.
   *
   * @return \Symfony\Component\Ldap\Entry|null
   *   LDAP Entry.
   */
  public function sanitizeUserDataResponse(Entry $entry, string $drupal_username) : ?Entry {
    if ($this->server
      ->get('bind_method') === 'anon_user') {
      return $entry;
    }

    // Filter out results with spaces added before or after, which are
    // considered OK by LDAP but are no good for us. Some setups have multiple
    // $nameAttribute per entry, so we loop through all possible options.
    $attribute_values = $entry
      ->getAttribute($this->server
      ->getAuthenticationNameAttribute(), FALSE);
    if ($attribute_values) {
      foreach ($attribute_values as $attribute_value) {
        if (mb_strtolower(trim($attribute_value)) === mb_strtolower($drupal_username)) {
          return $entry;
        }
      }
    }
    return NULL;
  }

  /**
   * Queries LDAP server for the user.
   *
   * @param string|null $base_dn
   *   Base DN.
   * @param string $drupal_username
   *   Drupal user name.
   *
   * @return \Symfony\Component\Ldap\Entry|false|null
   *   LDAP entry.
   *
   * @todo This function does return data and check for validity of response.
   *  This makes responses difficult to parse and should be optimized.
   */
  public function queryLdapForUsername(?string $base_dn, string $drupal_username) {
    if (!$this
      ->checkAvailability()) {
      return FALSE;
    }
    if (empty($base_dn)) {
      return NULL;
    }
    $query = sprintf('(%s=%s)', $this->server
      ->getAuthenticationNameAttribute(), $this
      ->ldapEscapeFilter($drupal_username));
    try {

      // We are requesting regular and operational attributes with this filter
      // since some directories (e.g. OpenLDAP) have common overlays such as
      // "memberOf" in operational attributes.
      // @see https://www.drupal.org/i/2939308
      $ldap_response = $this->ldap
        ->query($base_dn, $query, [
        'filter' => [
          '*',
          '+',
        ],
      ])
        ->execute();
    } catch (LdapException $e) {

      // Must find exactly one user for authentication to work.
      $this->logger
        ->error('LDAP server query error %message', [
        '%message' => $e
          ->getMessage(),
      ]);
      return FALSE;
    }
    if ($ldap_response
      ->count() === 0) {
      return NULL;
    }
    if ($ldap_response
      ->count() !== 1) {

      // Must find exactly one user for authentication to work.
      $this->logger
        ->error('Error: %count users found with %filter under %base_dn.', [
        '%count' => $ldap_response
          ->count(),
        '%filter' => $query,
        '%base_dn' => $base_dn,
      ]);
      return NULL;
    }
    return $ldap_response
      ->toArray()[0];
  }

}

Members

Namesort descending Modifiers Type Description Overrides
LdapBaseManager::$entityTypeManager protected property Entity Type Manager.
LdapBaseManager::$ldap protected property Symfony Ldap.
LdapBaseManager::$ldapBridge protected property LDAP Bridge.
LdapBaseManager::$logger protected property Logger.
LdapBaseManager::$moduleHandler protected property Module handler.
LdapBaseManager::$server protected property Server.
LdapBaseManager::applyModificationsToEntry protected function Apply modifications to entry. 1
LdapBaseManager::checkAvailability protected function Check availability of service.
LdapBaseManager::checkDnExists public function Does dn exist for this server?
LdapBaseManager::checkDnExistsIncludeData public function Does dn exist for this server and what is its data?
LdapBaseManager::createLdapEntry public function Create LDAP entry. 1
LdapBaseManager::deleteLdapEntry public function Perform an LDAP delete.
LdapBaseManager::matchUsernameToExistingLdapEntry public function Match username to existing LDAP entry.
LdapBaseManager::modifyLdapEntry public function Modify attributes of LDAP entry.
LdapBaseManager::queryAllBaseDnLdapForUsername public function Queries LDAP server for the user.
LdapBaseManager::queryLdapForUsername public function Queries LDAP server for the user.
LdapBaseManager::sanitizeUserDataResponse public function Sanitize user data response.
LdapBaseManager::searchAllBaseDns public function Perform an LDAP search on all base dns and aggregate into one result.
LdapBaseManager::setServer public function Set server by ID.
LdapBaseManager::setServerById public function Set server by ID.
LdapBaseManager::__construct public function Constructor. 1
LdapTransformationTraits::ldapEscapeDn protected function Wrapper for ldap_escape().
LdapTransformationTraits::ldapEscapeFilter protected function Wrapper for ldap_escape().
LdapTransformationTraits::php56PolyfillLdapEscape public static function Stub implementation of the {@link ldap_escape()} function of ext-ldap.
LdapTransformationTraits::splitDnWithAttributes public static function Wrapper for ldap_explode_dn().
LdapTransformationTraits::splitDnWithValues public static function Wrapper for ldap_explode_dn().