. * */ /** * This class defines custom LDAP exceptions that will be used in the * main LDAP class. */ class LDAPException extends Exception { /** * A LDAPException may be constructed thanks to a message, or an error * code. If the given argument is an integer, the exception will be * produced with message: * * LDAP error: [errno] errmsg * * Otherwise, the provided message will be used. * * @param mixed $message */ public function __construct ($message) { if (is_int ($message)) { $message = 'LDAP error: [' . $message . '] ' . ldap_err2str($message); } debug_event('LDAP', 'Exception: ' . $message, 6); parent::__construct ($message); } } /** * This class handles all the contacts with a LDAP server */ class LDAP { /** * Constructor * * This should never be called */ public function __construct() { debug_event('LDAP', '__construct has been called. This should not happen', 2); } /** Utility functions */ /** * recompose_field * * This function allows custom fields like 'givenName sn' to be used. */ private static function recompose_field ($field, $entry) { $fields = explode(' ', $field); $values = array_map (function ($field) use ($entry) { return $entry[strtolower($field)][0]; }, $fields); $value = trim(implode(' ', $values)); debug_event('LDAP', "recomposed field `$field` to obtain `$value`", 6); return $value; } /** * clean_search_results * * This function is here to return a real array {number} => {field} => {value array} * instead of the custom LDAP search results provided by the ldap_* library. */ private static function clean_search_results ($sr) { $sr_clean = []; foreach (array_filter($sr, 'is_int', ARRAY_FILTER_USE_KEY) as $i => $result) { $sr_clean[$i] = []; foreach ($result as $field => $values) { if ($field == 'count' || is_int ($field)) { continue; } elseif ($field == 'dn') { $sr_clean[$i][$field] = $values; } else { $sr_clean[$i][$field] = array_filter($values, 'is_int', ARRAY_FILTER_USE_KEY); } } } return $sr_clean; } /** Actual LDAP functions */ /** * Connect to the LDAP * Note: This does not open a connection. It checks whether * the given parameters are plausibe and can be used to open a * connection as soon as one is needed. */ private static function connect () { if (! $url = AmpConfig::get('ldap_url')) { throw new LDAPException('Required configuration value missing: ldap_url'); } if (! $link = ldap_connect ($url)) { throw new LDAPException('Could not connect to ' . $url); } $protocol_version = AmpConfig::get('ldap_protocol_version', 3); if (! ldap_set_option ($link, LDAP_OPT_PROTOCOL_VERSION, $protocol_version)) { throw new LDAPException('Could not set option PROTOCOL_VERSION to ' . $protocol_version); } if (AmpConfig::get('ldap_start_tls', "false") != "false") { if (! ldap_start_tls ($link)) { throw new LDAPException('Could not use StartTLS'); } } return $link; } /** * Binds to the LDAP */ private static function bind ($link, $username = null, $password = null) { debug_event('LDAP', "binding with username `$username`", 5); if ($username === null && $password === null) { $username = AmpConfig::get('ldap_username', ''); $password = AmpConfig::get('ldap_password', ''); } if (! ldap_bind ($link, $username, $password)) { throw new LDAPException("Could not bind to server using username `$username`"); } } /** * Unbinds from the LDAP */ private static function unbind ($link) { ldap_unbind ($link); } /** * Read attributes for a DN from the LDAP */ private static function read ($link, $dn, $attrs = [], $filter='objectClass=*') { $attrs_json = json_encode ($attrs); debug_event('LDAP', "reading attributes $attrs_json in `$dn`", 5); if (! $result = ldap_read ($link, $dn, $filter, $attrs)) { throw new LDAPException("Could not read attributes `$attrs_json` for dn `$dn`"); } if (! $infos = ldap_get_entries ($link, $result)) { throw new LDAPException("Empty search result for dn `$dn`"); } return $infos[0]; } /** * Search for a DN in the LDAP */ private static function search ($link, $base_dn, $filter, $only_one_result = true) { debug_event('LDAP', "searching in `$base_dn` for `$filter`", 5); if (! $result = ldap_search ($link, $base_dn, $filter)) { throw new LDAPException(ldap_errno($link)); } $entries = ldap_get_entries ($link, $result); $entries = self::clean_search_results($entries); if ($only_one_result) { if (count ($entries) < 1) { throw new LDAPException("Empty search results for filter `$filter`"); } if (count ($entries) > 1) { throw new LDAPException("Too many search results for filter `$filter`"); } return $entries[0]; } else { return $entries; } } /** * ldap_auth * * This handles authentication against a LDAP server. * * @param string $username * @param string $password * @return array */ public static function auth ($username, $password) { try { $link = self::connect (); self::bind($link); /* Search for the user with given base_dn, filter, objectclass and username */ if (! $filter = AmpConfig::get('ldap_filter')) { throw new LDAPException('Required configuration value missing: ldap_filter'); } if (strpos ($filter, '%v') !== false) { $filter = str_replace('%v', $username, $filter); } else { $filter = "($filter=$username)"; // Backward compatibility } if (! $objectclass = AmpConfig::get('ldap_objectclass')) { throw new LDAPException('Required configuration value missing: ldap_objectclass'); } $search = "(&(objectclass=$objectclass)$filter)"; debug_event ('LDAP', 'search: ' . $search, 5); if (! $base_dn = AmpConfig::get('ldap_search_dn')) { throw new LDAPException('Required configuration value missing: ldap_search_dn'); } $user_entry = self::search ($link, $base_dn, $search, true); $user_dn = $user_entry['dn']; self::bind ($link, $user_dn, $password); /* Test if the user is in the required group (optional) */ if ($group_dn = AmpConfig::get('ldap_require_group')) { $member_attribute = AmpConfig::get('ldap_member_attribute', 'member'); $group_infos = self::read ($link, $group_dn, [$member_attribute]); if (! preg_grep("/^$user_dn\$/i", $group_infos[$member_attribute])) { throw new LDAPException("`$user_dn` is not member of the group `$group_dn`"); } } /* Obtain name and email field. Reconstruct name field to allow custom things like "givenName sn" */ $name_field = AmpConfig::get('ldap_name_field', 'cn'); $name = self::recompose_field($name_field, $user_entry); $email_field = AmpConfig::get('ldap_email_field', 'mail'); $email = $user_entry[$email_field][0]; $return_value = [ 'success' => true, 'type' => 'ldap', 'username' => $username, 'name' => $name, 'email' => $email ]; if (($state_field = AmpConfig::get('ldap_state_field')) !== null) { $return_value['state'] = $user_entry[$state_field][0]; } if (($city_field = AmpConfig::get('ldap_city_field')) !== null) { $return_value['city'] = $user_entry[$city_field][0]; } } catch (LDAPException $e) { $message = $e->getMessage(); debug_event('LDAP', 'Error during authentication: ' . $message, 3); $return_value = [ 'success' => false, 'error' => $message ]; } if (isset ($link)) { self::unbind ($link); } debug_event('LDAP', 'Return value of authentication: ' . json_encode($return_value), 5); return $return_value; } }