From 74381ddfb75e71ed57e6f9056fcaa67c702b283b Mon Sep 17 00:00:00 2001 From: Liam Morland <lkmorlan@uwaterloo.ca> Date: Thu, 4 Feb 2021 14:26:51 -0500 Subject: [PATCH] ISTWCMS-4591: Create service uw_ldap with class UWLdap --- config/schema/uw_cfg_common.schema.yml | 10 ++ src/Service/UWLdap.php | 207 +++++++++++++++++++++++++ uw_cfg_common.services.yml | 3 + 3 files changed, 220 insertions(+) create mode 100644 config/schema/uw_cfg_common.schema.yml create mode 100644 src/Service/UWLdap.php diff --git a/config/schema/uw_cfg_common.schema.yml b/config/schema/uw_cfg_common.schema.yml new file mode 100644 index 00000000..b3bb66d0 --- /dev/null +++ b/config/schema/uw_cfg_common.schema.yml @@ -0,0 +1,10 @@ +uw_cfg_common.ldap: + type: config_object + label: 'LDAP settings' + mapping: + user: + type: string + label: 'LDAP user name' + pwd: + type: string + label: 'LDAP password' diff --git a/src/Service/UWLdap.php b/src/Service/UWLdap.php new file mode 100644 index 00000000..8c0bfdb1 --- /dev/null +++ b/src/Service/UWLdap.php @@ -0,0 +1,207 @@ +<?php + +namespace Drupal\uw_cfg_common\Service; + +use Drupal\Component\Utility\Html; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Logger\LoggerChannelTrait; +use Drupal\Core\Session\AccountProxy; + +/** + * Class for interfacing with UW's LDAP infrastructure. + */ +class UWLdap { + + use LoggerChannelTrait; + + /** + * Information about the LDAP server. + * + * @var array + */ + protected $ldapServer = [ + 'server' => 'ldaps://ldap-nexus.uwaterloo.ca', + 'base_dn' => 'dc=nexus, dc=uwaterloo, dc=ca', + // The name of the UID field. + 'uid' => 'sAMAccountName', + // Config keys in uw_cfg_common.ldap from which to load the user and + // password. + 'user' => 'user', + 'pwd' => 'pwd', + ]; + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * The current user object. + * + * @var \Drupal\Core\Session\AccountProxy + */ + protected $currentUser; + + /** + * Constructor with dependency injection. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + * @param \Drupal\Core\Session\AccountProxy $current_user + * The current user object. + */ + public function __construct(ConfigFactoryInterface $config_factory, AccountProxy $current_user) { + $this->configFactory = $config_factory; + $this->currentUser = $current_user; + } + + /** + * Open a connection to the LDAP server. + * + * @return bool + * TRUE if the connection succeeded, FALSE otherwise. + */ + protected function openConnection(): bool { + // Return early if opening a connection has already been attempted. It will + // not retry after a failed connection attempt. + if (isset($this->ldapServer['connection'])) { + return (bool) $this->ldapServer['connection']; + } + + // Load LDAP user and password from config. + foreach (['user', 'pwd'] as $field) { + if (isset($this->ldapServer[$field])) { + $this->ldapServer[$field] = $this->configFactory->get('uw_cfg_common.ldap')->get($this->ldapServer[$field]); + } + else { + $this->ldapServer[$field] = NULL; + } + } + + // Do not attempt connection if no username or password. + if (!isset($this->ldapServer['user']) || !isset($this->ldapServer['pwd'])) { + $this->getLogger('uw_cfg_common')->error('Unable to connect to LDAP server. Missing username or password.'); + $this->ldapServer['connection'] = FALSE; + return FALSE; + } + + // Connect and bind to the server. Log errors if any. + $ldap_connect = ldap_connect($this->ldapServer['server']); + if (!$ldap_connect || !ldap_bind($ldap_connect, $this->ldapServer['user'], $this->ldapServer['pwd'])) { + // Log errors on failure. + ldap_get_option($ldap_connect, LDAP_OPT_ERROR_NUMBER, $error_number); + ldap_get_option($ldap_connect, LDAP_OPT_DIAGNOSTIC_MESSAGE, $error_message); + ldap_get_option($ldap_connect, LDAP_OPT_ERROR_STRING, $error_string); + $variables = [ + '@error_number' => $error_number, + '@error_message' => $error_message, + '@error_string' => $error_string, + ]; + $this->getLogger('uw_cfg_common')->error('Unable to connect to LDAP server. Error number: @error_number; Error message: @error_message; Error string: @error_string', $variables); + $ldap_connect = FALSE; + } + // Store connection object. + $this->ldapServer['connection'] = $ldap_connect; + + return (bool) $ldap_connect; + } + + /** + * Wrapper for ldap_search() and ldap_get_entries(). + * + * @param string $filter + * Filter for the LDAP search. + * @param array $attributes + * Attributes to the LDAP search. + * + * @return array|null + * Array of results from LDAP search or NULL on error. + */ + public function search(string $filter, array $attributes = []): ?array { + if (!$this->openConnection()) { + return NULL; + } + + // Attempt search and get the entries if successful. Otherwise, return NULL. + $results = ldap_search($this->ldapServer['connection'], $this->ldapServer['base_dn'], $filter, $attributes); + if ($results) { + $results = ldap_get_entries($this->ldapServer['connection'], $results); + if ($results) { + return $results; + } + } + return NULL; + } + + /** + * Do an LDAP lookup for a person. + * + * @param string $username + * The user to lookup. + * + * @return array|null + * An array of LDAP results or NULL on failure. + */ + public function lookupPerson(string $username): ?array { + // Configure the userid filter. + $filter = '(' . $this->ldapServer['uid'] . '=' . $username . ')'; + + $result = $this->search($filter); + + // Return formatted results if the search is successful. + if (isset($result[0])) { + return $this->formatResults($result[0]); + } + return NULL; + } + + /** + * Make formatting changes to LDAP query results. + * + * @param array $results + * The LDAP query results. + * + * @return array + * The input array with formatting changes. + */ + public static function formatResults(array $results): array { + foreach ($results as $key => $value) { + if (is_array($value)) { + foreach ($value as $sub_key => $sub_value) { + if (is_string($sub_value)) { + if (in_array($key, ['objectguid', 'objectsid', 'sidhistory'], TRUE)) { + $results[$key][$sub_key] = self::formatGuid($sub_value); + } + else { + $results[$key][$sub_key] = utf8_encode($sub_value); + } + } + } + } + } + return $results; + } + + /** + * Converts a binary guid into a string representation. + * + * @param string $guid + * The guid as a binary string. + * + * @return string + * The guid as a displayable string. + */ + protected static function formatGuid(string $guid): string { + $guid = bin2hex($guid); + + // Format $guid so it matches what other LDAP tool displays for AD GUIDs. + if (strlen($guid) === 32) { + $guid = strtoupper(substr($guid, 6, 2) . substr($guid, 4, 2) . substr($guid, 2, 2) . substr($guid, 0, 2) . '-' . substr($guid, 10, 2) . substr($guid, 8, 2) . '-' . substr($guid, 14, 2) . substr($guid, 12, 2) . '-' . substr($guid, 16, 4) . '-' . substr($guid, 20)); + } + + return $guid; + } + +} diff --git a/uw_cfg_common.services.yml b/uw_cfg_common.services.yml index 49c1f917..f2363900 100644 --- a/uw_cfg_common.services.yml +++ b/uw_cfg_common.services.yml @@ -1,4 +1,7 @@ services: + uw_cfg_common.uw_ldap: + class: Drupal\uw_cfg_common\Service\UWLdap + arguments: ['@config.factory', '@current_user'] uw_cfg_common.uw_service: class: Drupal\uw_cfg_common\Service\UWService arguments: ['@entity_type.manager', '@database', '@simplify_menu.menu_items', '@path_alias.manager'] -- GitLab