ERROR: PHP's 'ldap' extension is not installed/enabled. ". "Please check your web server configuration.


\n"); } // Transfer the values from the config variables into a local // associative array, turning them all into arrays self::$config_items = array('ldap_host', 'ldap_port', 'ldap_base_dn', 'ldap_user_attrib', 'ldap_dn_search_attrib', 'ldap_dn_search_dn', 'ldap_dn_search_password', 'ldap_filter', 'ldap_group_member_attrib', 'ldap_admin_group_dn', 'ldap_v3', 'ldap_tls', 'ldap_email_attrib', 'ldap_name_attrib', 'ldap_disable_referrals', 'ldap_deref', 'ldap_filter_base_dn', 'ldap_filter_user_attr', 'ldap_client_cert', 'ldap_client_key' ); self::$all_ldap_opts = array(); // Get the array items (we'll handle the non-array items in a moment) and check // that they all have the same length $count = null; foreach (self::$config_items as $item) { if (isset($$item) && is_array($$item)) { self::$all_ldap_opts[$item] = $$item; if (isset($count)) { if (count($$item) != $count) { Errors::fatalError("MRBS configuration error: Count of LDAP array config variables doesn't match, aborting!"); } } else { $count = count($$item); } } } // Turn any non-array config items into arrays if (!isset($count)) { $count = 1; } foreach (self::$config_items as $item) { if (isset($$item) && !is_array($$item)) { self::$all_ldap_opts[$item] = array_fill(0, $count, $$item); } } } public function validateUser( #[\SensitiveParameter] ?string $user, #[\SensitiveParameter] ?string $pass) { // Check if we do not have a username/password // User can always bind to LDAP anonymously with empty password, // therefore we need to block empty password here... if (!isset($user) || !isset($pass) || strlen($pass)==0) { self::debug('empty username or password passed'); return false; } $object = array(); $object['pass'] = $pass; return ($this->action('validateUserCallback', $user, $object)) ? $user : false; } /* validateUserCallback(&$ldap, $base_dn, $dn, $user_search, $user, &$object) * * Checks if the specified username/password pair are valid * * &$ldap - Reference to the LDAP object * $base_dn - The base DN * $dn - The user's DN * $user_search - The LDAP filter to find the user * $user - The user name * &$object - Reference to the generic object * * Returns: * false - Didn't find a user * true - Found a user */ private static function validateUserCallback(&$ldap, $base_dn, $dn, $user_search, $user, &$object) { self::debug("base_dn '$base_dn' dn '$dn' user '$user'"); $pass = $object['pass']; // try an authenticated bind // use this to confirm that the user/password pair if ($dn && self::ldapBind($ldap, $dn, $pass)) { // however if there is a filter check that the // user is part of the group defined by the filter if (!isset($object['config']['ldap_filter']) || ($object['config']['ldap_filter'] === '')) { self::debug("successful authenticated bind with no \$ldap_filter"); return true; } else { // If we've got a search DN and password, then bind again using those credentials because // it's possible that the user doesn't have read access in the directory, even for their own // entry, in which case we'll get a "No such object" result. if (isset($object['config']['ldap_dn_search_dn']) && isset($object['config']['ldap_dn_search_password'])) { self::debug("rebinding as '" . $object['config']['ldap_dn_search_dn'] . "'"); if (!self::ldapBind($ldap, $object['config']['ldap_dn_search_dn'], $object['config']['ldap_dn_search_password'])) { self::debug("rebinding failed: " . self::ldapError($ldap)); return false; } self::debug('rebinding successful'); } $filter = $object['config']['ldap_filter']; self::debug("successful authenticated bind checking '$filter'"); // If ldap_filter_base_dn is set, set the filter to search for the user // in the given base_dn (OpenLDAP). If not, read from the user // attribute (AD) if (isset($object['config']['ldap_filter_base_dn'])) { $f = "(&(". $object['config']['ldap_filter_user_attr']. "=$user)($filter))"; $filter_dn = $object['config']['ldap_filter_base_dn']; $call = 'ldap_search'; } else { $f = "($filter)"; $filter_dn = $dn; $call = 'ldap_read'; } self::debug("trying filter: $f: dn: $filter_dn: method: $call"); $res = $call( $ldap, $filter_dn, $f, array() ); if (ldap_count_entries($ldap, $res) > 0) { self::debug('found entry with filter'); return true; } self::debug('no entry found with filter'); } } else { self::debug("bind to '$dn' failed: ". self::ldapError($ldap)); } // return failure if no connection is established return false; } protected function getUserFresh(string $username) : ?User { if (!isset($username) || ($username === '')) { return null; } $object = array(); $res = $this->action('getUserCallback', $username, $object); if (!$res || !isset($object['user'])) { return null; } // Use $object['user']['username'] rather than $username because they won't necessarily be // the same. See https://sourceforge.net/p/mrbs/bugs/518/ $user = parent::getUserFresh($object['user']['username']); $keys = array('display_name', 'email', 'level'); foreach ($keys as $key) { if (isset($object['user'][$key])) { $user->$key = $object['user'][$key]; } } return $user; } /* getUserCallback(&$ldap, $base_dn, $dn, $user_search, $username, &$object) * * &$ldap - Reference to the LDAP object * $base_dn - The base DN * $dn - The user's DN * $user_search - The LDAP filter to find the user * $username - The user name * &$object - Reference to the generic object * * Returns: * false - Didn't find a user * true - Found a user */ private static function getUserCallback(&$ldap, $base_dn, $dn, $user_search, $user, &$object) { global $ldap_get_user_email, $ldap_debug_attributes, $max_level; self::debug("base_dn '$base_dn' dn '$dn' user_search '$user_search' user '$user'"); if (!$ldap || !$base_dn || !$dn || !$user_search) { self::debug("invalid parameters, could not call ldap_read, returning false"); return false; } $attributes = self::getAttributes($object, $ldap_get_user_email, true); self::resetProfileClock(); // We suppress the errors because it's possible to get a "No such object" error if // the DN doesn't exist - which it won't if (a) we're searching an array of LDAP hosts // or (b) the DN has been deleted since the booking was made. But check the error // code afterwards and trigger an error if it was any other kind of error. $res = @ldap_read( $ldap, $dn, "(objectclass=*)", array_values($attributes), 0, 1 ); $t = self::getProfileClock(); if ($res === false) { self::debug("ldap_read() failed: " . self::ldapError($ldap)); if (self::LDAP_NO_SUCH_OBJECT !== ($errno = ldap_errno($ldap))) { if ($errno === self::LDAP_SUCCESS) { // The errno is reporting success, but that's just the ldap errno. If ldap_read() // doesn't get as far as interrogating the directory, it will return false, // but ldap_errno() will return success. To get more details try temporarily // removing the error suppression operator ('@'). $message = "ldap_read() failed, not due to an LDAP error but probably due " . "to an initialization error such as 'Array initialization wrong'"; } else { $message = ldap_err2str($errno); } trigger_error($message, E_USER_WARNING); } return false; } $n_entries = ldap_count_entries($ldap, $res); if ($n_entries === false) { self::debug("No entries found - ldap_count_entries() error"); return false; } self::debug("$n_entries entries found"); if ($n_entries === 0) { return false; } self::debug("ldap_read() succeeded, taking $t seconds"); if ($ldap_debug_attributes) { // Repeat the read, this time fetching all the attributes and then write // the attributes and their values to the debug log. Useful for discovering // attribute names. $res2 = @ldap_read($ldap, $dn, "(objectclass=*)", [], 0, 1); $entry = ldap_first_entry($ldap, $res2); $attribute = ldap_first_attribute($ldap, $entry); while ($attribute) { $values = ldap_get_values($ldap, $entry, $attribute); unset($values['count']); // We don't need this element self::debug("Attribute: \"$attribute\"; Value(s): \"" . implode('", "', $values) . '"'); $attribute = ldap_next_attribute($ldap, $entry); } } $entry = ldap_first_entry($ldap, $res); $user = self::getResult($ldap, $entry, $attributes); if (!isset($user['username'])) { $message = 'No username found. Check the value of $ldap_user_attrib in the MRBS config file.'; $message .= " It is currently set to '" . $object['config']['ldap_user_attrib'] . "'."; self::debug($message); return false; } if (!isset($user['display_name'])) { $user['display_name'] = $user['username']; } if (isset($user['groups'])) { if (isset($object['config']['ldap_admin_group_dn'])) { $user['level'] = in_arrayi($object['config']['ldap_admin_group_dn'], $user['groups']) ? $max_level : 1; } } self::debug("User '" . $user['username'] . "' found"); $object['user'] = $user; return true; } public function getUsernames() { $mrbs_user = session()->getCurrentUser(); if (!isset($mrbs_user)) { return false; } $object = array(); $object['users'] = array(); $users = array(); $res = $this->action('getUsernamesCallback', $mrbs_user->username, $object, true); if ($res === false) { trigger_error("MRBS: could not get LDAP usernames.", E_USER_WARNING); return false; } if (isset($object['users'])) { $users = $object['users']; } self::sortUsers($users); return $users; } private static function getUsernamesCallback(&$ldap, $base_dn, $dn, $user_search, $user, &$object) { self::debug("base_dn '$base_dn'"); if (!$ldap || !$base_dn || !isset($object['config']['ldap_user_attrib'])) { self::debug("invalid parameters, could not call ldap_search, returning false"); return false; } if (isset($object['config']['ldap_filter'])) { $filter = $object['config']['ldap_filter']; } else { $filter = 'objectclass=*'; } $filter = "($filter)"; // Form the attributes $username_attrib = mb_strtolower($object['config']['ldap_user_attrib']); $attributes = array($username_attrib); // The display name attribute might not have been set in the config file if (isset($object['config']['ldap_name_attrib'])) { $display_name_attrib = mb_strtolower($object['config']['ldap_name_attrib']); $attributes[] = $display_name_attrib; } self::debug("searching with base_dn '$base_dn' and filter '$filter'"); self::resetProfileClock(); $res = ldap_search($ldap, $base_dn, $filter, $attributes); $t = self::getProfileClock(); if ($res === false) { self::debug("ldap_search failed: " . self::ldapError($ldap)); return false; } self::debug(ldap_count_entries($ldap, $res) . " entries found in $t seconds"); $entry = ldap_first_entry($ldap, $res); // Loop through the entries to get all the users while ($entry) { // Initialise all keys in the user array to NULL, in case an attribute isn't present $user = array('username' => null, 'display_name' => null); $attribute = ldap_first_attribute($ldap, $entry); // Loop through all the attributes for this user while ($attribute) { $values = ldap_get_values($ldap, $entry, $attribute); $attribute = mb_strtolower($attribute); // ready for the comparisons if ($attribute == $username_attrib) { $user['username'] = $values[0]; } elseif ($attribute == $display_name_attrib) { $user['display_name'] = $values[0]; } $attribute = ldap_next_attribute($ldap, $entry); } if (isset($user['username'])) { if (!isset($user['display_name'])) { $user['display_name'] = $user['username']; } $object['users'][] = $user; } $entry = ldap_next_entry($ldap, $entry); } return true; } // Returns an array of attributes for use in an LDAP query private static function getAttributes(array $object, bool $include_email=true, bool $include_groups=true) : array { $result = array(); // Username $result['username'] = mb_strtolower($object['config']['ldap_user_attrib']); // The display name attribute might not have been set in the config file if (isset($object['config']['ldap_name_attrib'])) { $result['display_name'] = mb_strtolower($object['config']['ldap_name_attrib']); } // The email address if ($include_email && isset($object['config']['ldap_email_attrib'])) { $result['email'] = mb_strtolower($object['config']['ldap_email_attrib']); } // The group name attribute might not have been set in the config file if ($include_groups && isset($object['config']['ldap_group_member_attrib'])) { $result['groups'] = mb_strtolower($object['config']['ldap_group_member_attrib']); } return $result; } // Returns an associative array from the result of an LDAP search private static function getResult($ldap, $entry, array $attributes) : array { // Initialise all keys in the user array, in case an attribute isn't present $attributes_keys = array_keys($attributes); $user = array(); foreach ($attributes_keys as $key) { switch ($key) { case 'username': case 'display_name': case 'email': $user[$key] = null; break; case 'groups': $user[$key] = array(); break; default: throw new Exception("Unknown key '$key'"); } } $attribute = ldap_first_attribute($ldap, $entry); // Loop through all the attributes for this user while ($attribute) { $values = ldap_get_values($ldap, $entry, $attribute); $attribute = mb_strtolower($attribute); // ready for the comparisons if ($attribute == $attributes['username']) { $user['username'] = $values[0]; } elseif (isset($attributes['display_name']) && ($attribute == $attributes['display_name'])) { $user['display_name'] = $values[0]; } elseif (isset($attributes['email']) && ($attribute == $attributes['email'])) { $user['email'] = $values[0]; } elseif (isset($attributes['groups']) && ($attribute == $attributes['groups'])) { for ($i=0; $i<$values['count']; $i++) { $user['groups'][] = $values[$i]; } } $attribute = ldap_next_attribute($ldap, $entry); } return $user; } /* action($callback, $username, &$object) * * Connects/binds to all configured LDAP servers/base DNs and * then performs a callback, passing the LDAP object, $base_dn, * user DN (in $dn), $username and a generic object $object * * $callback - The callback function * $username - The user name * &$object - Reference to the generic object, type defined by caller * $keep_going - Don't stop when a user has been found, but keep going through all the LDAP * hosts. Useful, for example, when you want to get a list of all users. * * Returns: * boolean - Whether the action was successful */ public function action(string $callback, string $username, &$object, bool $keep_going=false) : bool { global $ldap_unbind_between_attempts; $result = false; for ($idx=0; $idx < count(self::$all_ldap_opts['ldap_host']); $idx++) { // Establish LDAP connection $uri = self::getUri($idx); $ldap = ldap_connect($uri); // Check that connection was established if ($ldap) { self::debug("got LDAP connection using $uri"); // Set any applicable LDAP options self::setOptions($ldap, $idx); if (isset(self::$all_ldap_opts['ldap_dn_search_attrib'][$idx])) { if (isset(self::$all_ldap_opts['ldap_dn_search_dn'][$idx]) && isset(self::$all_ldap_opts['ldap_dn_search_password'][$idx])) { // Bind with DN and password self::debug("binding with search_dn and search_password"); $res = self::ldapBind($ldap, self::$all_ldap_opts['ldap_dn_search_dn'][$idx], self::$all_ldap_opts['ldap_dn_search_password'][$idx]); } else { // Anonymous bind self::debug("binding anonymously"); $res = self::ldapBind($ldap); } if (!$res) { self::debug("initial bind failed: " . self::ldapError($ldap)); } else { self::debug("initial bind was successful"); $base_dn = self::$all_ldap_opts['ldap_base_dn'][$idx]; $filter = "(" . self::$all_ldap_opts['ldap_dn_search_attrib'][$idx] . "=$username)"; self::debug("searching using base_dn '$base_dn' and filter '$filter'"); $res = ldap_search($ldap, $base_dn, $filter); if ($res === false) { self::debug("ldap_search failed: ". self::ldapError($ldap)); } else { if (ldap_count_entries($ldap, $res) == 1) { $entries = ldap_get_entries($ldap, $res); $dn = $entries[0]["dn"]; $user_search = "distinguishedName=" . $dn; self::debug("found one entry dn '$dn'"); } else { self::debug(ldap_count_entries($ldap, $res) . " entries found, no unique dn"); } } } } else { // construct dn for user $user_search = self::$all_ldap_opts['ldap_user_attrib'][$idx] . "=" . $username; $dn = $user_search . "," . self::$all_ldap_opts['ldap_base_dn'][$idx]; self::debug("constructed dn '$dn' and " . "user_search '$user_search' using '" . self::$all_ldap_opts['ldap_user_attrib'][$idx] . "'"); } foreach (self::$config_items as $item) { if (isset(self::$all_ldap_opts[$item][$idx])) { $object['config'][$item] = self::$all_ldap_opts[$item][$idx]; } else { $object['config'][$item] = null; } } if (empty($dn)) { self::debug("no DN determined, not calling callback"); } else { $res = self::$callback($ldap, self::$all_ldap_opts['ldap_base_dn'][$idx], $dn, $user_search, $username, $object); if ($res) { $result = true; } } if ($ldap_unbind_between_attempts) { self::debug("unbinding from $uri"); ldap_unbind($ldap); } } // if ($ldap) if ($result && !$keep_going) { return true; } } // for () return $result; } // A wrapper for ldap_bind() that optionally suppresses "invalid credentials" errors. // The SensitiveParameter attribute needs to be on a separate line for PHP 7. // The attribute is only recognised by PHP 8.2 and later. private static function ldapBind ( $link_identifier, ?string $bind_rdn=null, #[\SensitiveParameter] ?string $bind_password=null ) : bool { global $ldap_suppress_invalid_credentials; // Suppress all errors and then look to see what the error was and then // trigger the error again, depending on config settings. $result = @ldap_bind($link_identifier, $bind_rdn, $bind_password); if (!$result) { $errno = ldap_errno($link_identifier); if (!$ldap_suppress_invalid_credentials || ($errno != self::LDAP_INVALID_CREDENTIALS)) { trigger_error(ldap_err2str($errno), E_USER_WARNING); } } return $result; } // Gets the full LDAP URI private static function getUri(int $idx) : string { // First get the scheme and host $host = self::$all_ldap_opts['ldap_host'][$idx]; $parsed_url = parse_url($host); if (isset($parsed_url['scheme'])) { $scheme = $parsed_url['scheme']; $host = $parsed_url['host']; } // If we haven't got a scheme then make an educated guess based on the port. if (!isset($scheme)) { // And if there isn't a port defined either then use a sensible default $port = self::$all_ldap_opts['ldap_port'][$idx] ?? self::DEFAULT_PORT_LDAP; $scheme = ($port == self::DEFAULT_PORT_LDAPS) ? 'ldaps' : 'ldap'; } // If we have got a scheme then get the port. If it has been defined explicitly in the // config file, then use that. Otherwise make an educated guess based on the scheme. else { if (isset(self::$all_ldap_opts['ldap_port'][$idx])) { $port = self::$all_ldap_opts['ldap_port'][$idx]; } else { $port = ($scheme == 'ldaps') ? self::DEFAULT_PORT_LDAPS : self::DEFAULT_PORT_LDAP; } } return "$scheme://$host:$port"; } private static function setOptions($ldap, int $idx) : void { if (isset(self::$all_ldap_opts['ldap_deref'][$idx])) { ldap_set_option($ldap, LDAP_OPT_DEREF, self::$all_ldap_opts['ldap_deref'][$idx]); } if (isset(self::$all_ldap_opts['ldap_v3'][$idx]) && self::$all_ldap_opts['ldap_v3'][$idx]) { ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3); } if (isset(self::$all_ldap_opts['ldap_client_cert'][$idx]) && self::$all_ldap_opts['ldap_client_cert'][$idx]) { // Requires PHP 7.1.0 or later ldap_set_option($ldap, LDAP_OPT_X_TLS_CERTFILE, self::$all_ldap_opts['ldap_client_cert'][$idx]); } if (isset(self::$all_ldap_opts['ldap_client_key'][$idx]) && self::$all_ldap_opts['ldap_client_key'][$idx]) { // Requires PHP 7.1.0 or later ldap_set_option($ldap, LDAP_OPT_X_TLS_KEYFILE, self::$all_ldap_opts['ldap_client_key'][$idx]); } if (isset(self::$all_ldap_opts['ldap_tls'][$idx]) && self::$all_ldap_opts['ldap_tls'][$idx]) { ldap_start_tls($ldap); } if (isset(self::$all_ldap_opts['ldap_disable_referrals'][$idx]) && self::$all_ldap_opts['ldap_disable_referrals'][$idx]) { // Required to do a search on Active Directory for Win 2003+ ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0); } } // Adds extra diagnostic information to ldap_error() private static function ldapError ($link_identifier) : string { $result = ldap_error($link_identifier); // LDAP_OPT_DIAGNOSTIC_MESSAGE is not supported by all LDAP libraries if (defined('LDAP_OPT_DIAGNOSTIC_MESSAGE') && ldap_get_option($link_identifier, LDAP_OPT_DIAGNOSTIC_MESSAGE, $err) && isset($err) && ($err !== '')) { $result .= " [$err]"; } return $result; } /* debug($message) * * Output LDAP debugging, if either of the configuration variables * $ldap_debug or $ldap_debug_attributes is true. * */ private static function debug(string $message) : void { global $ldap_debug, $ldap_debug_attributes; if ($ldap_debug || $ldap_debug_attributes) { self::logDebugMessage($message); } } private static function getProfileClock() { global $ldap_debug; if ($ldap_debug) { return (get_microtime() - self::$profile_clock); } else { return null; } } private static function resetProfileClock() : void { global $ldap_debug; if ($ldap_debug) { self::$profile_clock = get_microtime(); } } }