1) { // Currently, OBS_DEBUG > 1 for WUI is not supported ;) // Disabled by default, VERY chatty. ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, 7); } // If a single server is specified, convert it to array anyway for use in functions below if (!is_array($config['auth_ldap_server'])) { // If no server set and domain is specified, get domain controllers from SRV records if ($config['auth_ldap_server'] == '' && $config['auth_ldap_ad_domain'] != '') { $config['auth_ldap_server'] = ldap_domain_servers_from_dns($config['auth_ldap_ad_domain']); } else { $config['auth_ldap_server'] = [ $config['auth_ldap_server'] ]; } } /** * Finds if user belongs to group, recursively if requested * Private function for this LDAP module only. * * @param string $ldap_group LDAP group to check * @param string $userdn User Distinguished Name * @param int $depth Recursion depth (used in recursion, stops at configured maximum depth) * * @return boolean */ function ldap_search_user($ldap_group, $userdn, $depth = -1) { global $ds, $config; if ($config['auth_ldap_groupreverse']) { $compare = ldap_internal_compare($ds, $userdn, $config['auth_ldap_attr']['memberOf'], $ldap_group); } else { $compare = ldap_internal_compare($ds, $ldap_group, $config['auth_ldap_groupmemberattr'], $userdn); } if ($compare === TRUE) { return TRUE; // Member found, return TRUE } if (!$config['auth_ldap_groupreverse'] && $config['auth_ldap_recursive'] && ($depth < $config['auth_ldap_recursive_maxdepth'])) { $depth++; //$filter = "(&(objectClass=group)(memberOf=". $ldap_group ."))"; $filter_params = array(); $filter_params[] = ldap_filter_create('objectClass', $config['auth_ldap_attr']['group']); $filter_params[] = ldap_filter_create($config['auth_ldap_attr']['memberOf'], $ldap_group); $filter = ldap_filter_combine($filter_params); print_debug("LDAP[UserSearch][$depth][Comparing: " . $ldap_group . "][".$config['auth_ldap_groupmemberattr']."=$userdn][Filter: $filter]"); $ldap_search = ldap_search($ds, trim($config['auth_ldap_groupbase'], ', '), $filter, array($config['auth_ldap_attr']['dn'])); //r($filter); if (ldap_internal_is_valid($ldap_search)) { $ldap_results = ldap_get_entries($ds, $ldap_search); //r($ldap_results); array_shift($ldap_results); // Chop off "count" array entry foreach ($ldap_results as $element) { if (!isset($element[$config['auth_ldap_attr']['dn']])) { continue; } // Not sure, seems as different results in LDAP vs AD // See: https://jira.observium.org/browse/OBS-3240 and https://jira.observium.org/browse/OBS-3310 $element_dn = is_array($element[$config['auth_ldap_attr']['dn']]) ? $element[$config['auth_ldap_attr']['dn']][0] : $element[$config['auth_ldap_attr']['dn']]; print_debug("LDAP[UserSearch][$depth][Comparing: " . $element_dn . "][" . $config['auth_ldap_groupmemberattr'] . "=$userdn]"); $result = ldap_search_user($element_dn, $userdn, $depth); if ($result === TRUE) { return TRUE; // Member found, return TRUE } } } return FALSE; // Not found, return FALSE. } return FALSE; // Recursion disabled or reached maximum depth, return FALSE. } /** * Initializes the LDAP connection to the specified server(s). Cycles through all servers, throws error when no server can be reached. * Private function for this LDAP module only. */ function ldap_init() { global $ds, $config; if (!ldap_internal_is_valid($ds)) { print_debug('LDAP[Connecting to ' . implode(' ',$config['auth_ldap_server']) . ']'); if ($config['auth_ldap_port'] === 636) { print_debug('LDAP[Port 636. Prepending ldaps:// to server URI]'); $ds = @ldap_connect(implode(' ',preg_filter('/^(ldaps:\/\/)?/', 'ldaps://', $config['auth_ldap_server'])), $config['auth_ldap_port']); } else { $ds = @ldap_connect(implode(' ',$config['auth_ldap_server']), $config['auth_ldap_port']); } print_debug("LDAP[Connected]"); if ($config['auth_ldap_starttls'] && (in_array($config['auth_ldap_starttls'], [ 'optional', 'require', '1', 1, TRUE ], TRUE))) { $tls = ldap_start_tls($ds); if ($config['auth_ldap_starttls'] === 'require' && !$tls) { session_logout(); print_error("Fatal error: LDAP TLS required but not successfully negotiated [" . ldap_error($ds) . "]"); exit; } } if ($config['auth_ldap_referrals']) { ldap_set_option($ds, LDAP_OPT_REFERRALS, $config['auth_ldap_referrals']); print_debug("LDAP[Referrals][Set to " . $config['auth_ldap_referrals'] . "]"); } else { ldap_set_option($ds, LDAP_OPT_REFERRALS, FALSE); print_debug("LDAP[Referrals][Disabled]"); } if ($config['auth_ldap_version']) { ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, $config['auth_ldap_version']); print_debug("LDAP[Version][Set to " . $config['auth_ldap_version'] . "]"); } } } /** * Check username and password against LDAP authentication backend. * Cut short if remote_user setting is on, as we assume the user has already authed against Apache. * We still need to check for certain group memberships however, so we can not simply bail out with TRUE in such case. * * @param string $username User name to check * @param string $password User password to check * @return int Authentication success (0 = fail, 1 = success) FIXME bool */ function ldap_authenticate($username, $password) { global $config, $ds; ldap_init(); if ($username && $ds) { if (ldap_bind_dn($username, $password)) { return 0; } $binduser = ldap_internal_dn_from_username($username); if ($binduser) { print_debug("LDAP[Authenticate][User: $username][Bind user: $binduser]"); // Auth via Apache + LDAP fallback -> automatically authenticated, fall through to group permission check if ($config['auth']['remote_user'] || ldap_bind($ds, $binduser, $password)) { if (!$config['auth_ldap_group']) { // No groups defined, auth is sufficient return 1; } $userdn = ($config['auth_ldap_groupmembertype'] === 'fulldn' ? $binduser : $username); foreach ($config['auth_ldap_group'] as $ldap_group) { if ($config['auth_ldap_groupreverse']) { print_debug("LDAP[Authenticate][Comparing: " . $userdn . "][".$config['auth_ldap_attr']['memberOf']."=$ldap_group]"); } else { print_debug("LDAP[Authenticate][Comparing: " . $ldap_group . "][".$config['auth_ldap_groupmemberattr']."=$userdn]"); } $compare = ldap_search_user($ldap_group, $userdn); if ($compare === -1) { print_debug("LDAP[Authenticate][Compare LDAP error: " . ldap_error($ds) . "]"); continue; } if ($compare === FALSE) { print_debug("LDAP[Authenticate][Processing group: $ldap_group][Not matched]"); } else { // $compare === TRUE print_debug("LDAP[Authenticate][Processing group: $ldap_group][Matched]"); return 1; } } // Restore bind dn when used binddn or bindanonymous // https://jira.observium.org/browse/OBS-1976 if (!$config['auth']['remote_user'] && ($config['auth_ldap_binddn'] || $config['auth_ldap_bindanonymous'])) { unset($GLOBALS['cache']['ldap']['bind_result']); ldap_bind_dn(); } } else { print_debug(ldap_error($ds)); } } } //session_logout(); return 0; } /** * Check if the backend allows users to log out. * We don't check for Apache authentication (remote_user) as this is done already before calling into this function. * * @return bool TRUE if logout is possible, FALSE if it is not */ function ldap_auth_can_logout() { return TRUE; } /** * Check if the backend allows a specific user to change their password. * This is not currently possible using the LDAP backend. * * @param string $username Username to check * @return bool TRUE if password change is possible, FALSE if it is not */ function ldap_auth_can_change_password($username = "") { return 0; } /** * Changes a user's password. * This is not currently possible using the LDAP backend. * * @param string $username Username to modify the password for * @param string $password New password * @return bool TRUE if password change is successful, FALSE if it is not */ function ldap_auth_change_password($username, $newpassword) { // Not supported (for now?) return FALSE; } /** * Check if the backend allows user management at all (create/delete/modify users). * This is not currently possible using the LDAP backend. * * @return bool TRUE if user management is possible, FALSE if it is not */ function ldap_auth_usermanagement() { return 0; } /** * Adds a new user to the user backend. * This is not currently possible using the LDAP backend. * * @param string $username User's username * @param string $password User's password (plain text) * @param int $level User's auth level * @param string $email User's e-mail address * @param string $realname User's real name * @param bool $can_modify_passwd TRUE if user can modify their own password, FALSE if not * @param string $description User's description * @return bool TRUE if user addition is successful, FALSE if it is not */ function ldap_adduser($username, $password, $level, $email = "", $realname = "", $can_modify_passwd = '1') { // Not supported return FALSE; } /** * Check if a user, specified by username, exists in the user backend. * * @param string $username Username to check * @return bool TRUE if the user exists, FALSE if they do not */ function ldap_auth_user_exists($username) { global $config, $ds; ldap_init(); if (ldap_bind_dn()) { return 0; } // Will not work without bind user or anon bind $binduser = ldap_internal_dn_from_username($username); if ($binduser) { return 1; } return 0; } /** * Retrieve user auth level for specified user. * * @param string $username Username to retrieve the auth level for * @return int User's auth level */ function ldap_auth_user_level($username) { global $config, $ds, $cache; if (!isset($cache['ldap']['level'][$username])) { $userlevel = 0; ldap_init(); ldap_bind_dn(); // Find all defined groups $username is in $userdn = (strtolower($config['auth_ldap_groupmembertype']) === 'fulldn' ? ldap_internal_dn_from_username($username) : $username); print_debug("LDAP[UserLevel][UserDN: $userdn]"); // This used to be done with a filter, but AD seems to be really retarded with regards to escaping. // // Particularly: // CN=Name\, User,OU=Team,OU=Region,OU=Employees,DC=corp,DC=example,DC=com // Has 2 methods of escaping, we automatically do the first: // CN=Name\2C, User,OU=Team,OU=Region,OU=Employees,DC=corp,DC=example,DC=com // Yet the filter used here before only worked doing this: // CN=Name\\, User,OU=Team,OU=Region,OU=Employees,DC=corp,DC=example,DC=com // // Yay for arbitrary escapes. Don't know how to handle; this is most likely (hopefully) AD specific. // So, we foreach our locally known groups instead. foreach ($config['auth_ldap_groups'] as $ldap_group => $ldap_group_info) { if (!str_contains($ldap_group, '=')) { print_debug("WARNING: You specified LDAP group '$ldap_group' without full DN syntax. Appending group base, this becomes 'CN=" . $ldap_group . ',' . $config['auth_ldap_groupbase'] . "'. If this is correct, you're in luck! If it's not, please check your configuration."); $ldap_group = 'CN=' . $ldap_group . ',' . $config['auth_ldap_groupbase']; } $compare = ldap_search_user($ldap_group, $userdn); if ($compare === -1) { print_debug("LDAP[UserLevel][Compare LDAP error: " . ldap_error($ds) . "]"); continue; } if ($compare === FALSE) { print_debug("LDAP[UserLevel][Processing group: $ldap_group][Not matched]"); } else { // $compare === TRUE print_debug("LDAP[UserLevel][Processing group: $ldap_group][Level: " . $ldap_group_info['level'] . "]"); if ($ldap_group_info['level'] > $userlevel) { $userlevel = $ldap_group_info['level']; print_debug("LDAP[UserLevel][Accepted group level as new highest level]"); } else { print_debug("LDAP[UserLevel][Ignoring group level as it's lower than what we have already]"); } } } print_debug("LDAP[Userlevel][Final level: $userlevel]"); $cache['ldap']['level'][$username] = $userlevel; } return $cache['ldap']['level'][$username]; } /** * Retrieve user id for specified user. * * @param string $username Username to retrieve the ID for * @return int User's ID */ function ldap_auth_user_id($username) { global $config, $ds; $userid = -1; ldap_init(); ldap_bind_dn(); $userdn = ($config['auth_ldap_groupmembertype'] == 'fulldn' ? ldap_internal_dn_from_username($username) : $config['auth_ldap_prefix'] . $username . $config['auth_ldap_suffix']); //$filter = "(" . str_ireplace($config['auth_ldap_suffix'], '', $userdn) . ")"; //$filter = "(&(objectClass=".$config['auth_ldap_objectclass'].")(".$config['auth_ldap_attr']['uid']."=" . $username . "))"; $filter_params = array(); $filter_params[] = ldap_filter_create('objectClass', $config['auth_ldap_objectclass']); $filter_params[] = ldap_filter_create($config['auth_ldap_attr']['uid'], $username); $filter = ldap_filter_combine($filter_params); print_debug("LDAP[Filter][$filter][" . trim($config['auth_ldap_suffix'], ', ') . "]"); $search = ldap_search($ds, trim($config['auth_ldap_suffix'], ', '), $filter); //r($search); $entries = ldap_internal_is_valid($search) ? ldap_get_entries($ds, $search) : []; //r($entries); if ($entries['count']) { $userid = ldap_internal_auth_user_id($entries[0]); print_debug("LDAP[UserID][$userid]"); } else { print_debug("LDAP[UserID][User not found through filter]"); } return $userid; } /** * Deletes a user from the user database. * This is not currently possible using the LDAP backend. * * @param string $username Username to delete * @return bool TRUE if user deletion is successful, FALSE if it is not */ function ldap_deluser($username) { // Call into mysql database functions to make sure user is gone from the database for legacy setups mysql_deluser($username); // Not supported return FALSE; } /** * Find the user's username by specifying their user ID. * * @param int $user_id The user's ID to look up the username for * @return string The user's user name, or FALSE if the user ID is not found */ function ldap_auth_username_by_id($user_id) { $userlist = ldap_auth_user_list(); foreach($userlist as $user) { if ($user['user_id'] == $user_id) { return $user['username']; } } return ""; // FIXME FALSE! } /** * Get the user information by username * * @param string $username Username * @return string The user's user name, or FALSE if the user ID is not found */ function ldap_auth_user_info($username) { $userinfo = array(); if (empty($username)) { return $userinfo; } $userlist = ldap_auth_user_list($username); foreach($userlist as $user) { if ($user['username'] == $username) { $userinfo = $user; break; } } return $userinfo; } /** * Retrieve list of users with all details. * * @return array Rows of user data */ function ldap_auth_user_list($username = NULL) { global $config, $ds; // Use caching for reduce queries to LDAP if (isset($GLOBALS['cache']['ldap']['userlist'])) { if (($config['time']['now'] - $GLOBALS['cache']['ldap']['userlist']['unixtime']) <= 300) { // Cache valid for 5 min //print_message('cached'); return $GLOBALS['cache']['ldap']['userlist']['entries']; } unset($GLOBALS['cache']['ldap']['userlist']); } ldap_init(); ldap_bind_dn(); //$filter = '(objectClass=' . $config['auth_ldap_objectclass'] . ')'; $filter_params = array(); $filter_params[] = ldap_filter_create('objectClass', $config['auth_ldap_objectclass']); if (!empty($username)) { // Filter users by username $filter_params[] = ldap_filter_create($config['auth_ldap_attr']['uid'], $username); } $ldap_group_count = safe_count($config['auth_ldap_group']); if ($ldap_group_count === 1) { //$filter = '(&'.$filter.'(memberof='.$config['auth_ldap_group'][0].'))'; $filter_params[] = ldap_filter_create($config['auth_ldap_attr']['memberOf'], $config['auth_ldap_group'][0]); } elseif ($ldap_group_count > 1) { $group_params = array(); foreach($config['auth_ldap_group'] as $group) { //$group_filter .= '(memberof='.$group.')'; $group_params[] = ldap_filter_create($config['auth_ldap_attr']['memberOf'], $group); } $filter_params[] = ldap_filter_combine($group_params, '|'); //$filter = '(&'.$filter.'(|'.$group_filter.'))'; } $filter = ldap_filter_combine($filter_params); // Limit fetched attributes, for reduce network transfer size $attributes = [ strtolower($config['auth_ldap_attr']['uid']), strtolower($config['auth_ldap_attr']['cn']), strtolower($config['auth_ldap_attr']['uidNumber']), 'description', 'mail', 'dn', ]; print_debug("LDAP[UserList][Filter][$filter][" . trim($config['auth_ldap_suffix'], ', ') . "]"); $entries = ldap_internal_paged_entries($filter, $attributes); //print_vars($entries); ldap_internal_user_entries($entries, $userlist); unset($entries); $GLOBALS['cache']['ldap']['userlist'] = [ 'unixtime' => $config['time']['now'], 'entries' => $userlist ]; return $userlist; } /** * Parse user entries in ldap_auth_user_list() * * @param array $entries LDAP entries by ldap_get_entries() * @param array $userlist Users list */ function ldap_internal_user_entries($entries, &$userlist) { global $config, $ds; if (!is_array($userlist)) { $userlist = []; } if ($entries['count']) { unset($entries['count']); //print_vars($entries); foreach ($entries as $i => $entry) { $username = $entry[strtolower($config['auth_ldap_attr']['uid'])][0]; $realname = $entry[strtolower($config['auth_ldap_attr']['cn'])][0]; $user_id = ldap_internal_auth_user_id($entry); $email = $entry['mail'][0]; $description = $entry['description'][0]; $userdn = (strtolower($config['auth_ldap_groupmembertype']) === 'fulldn' ? $entry['dn'] : $username); if ($config['auth_ldap_groupreverse']) { print_debug("LDAP[UserList][Compare: $userdn][".$config['auth_ldap_attr']['memberOf']."][" . implode('|', (array)$config['auth_ldap_group']) . "]"); } else { print_debug("LDAP[UserList][Compare: " . implode('|', (array)$config['auth_ldap_group']) . "][".$config['auth_ldap_groupmemberattr']."][$userdn]"); } //if (!is_numeric($user_id)) { print_vars($entry); continue; } $authorized = FALSE; foreach ($config['auth_ldap_group'] as $ldap_group) { $compare = ldap_search_user($ldap_group, $userdn); //print_warning("$username, $realname, "); //r($compare); if ($compare === -1) { print_debug("LDAP[UserList][Compare LDAP error: " . ldap_error($ds) . "]"); continue; } if ($compare === FALSE) { print_debug("LDAP[UserList][Processing group: $ldap_group][Not matched]"); } else { // $compare === TRUE print_debug("LDAP[UserList][Authorized: $userdn for group $ldap_group]"); $authorized = TRUE; break; } } if (!isset($config['auth_ldap_group']) || $authorized) { $user_level = ldap_auth_user_level($username); $userlist[] = [ 'username' => $username, 'realname' => $realname, 'user_id' => $user_id, 'level' => $user_level, 'email' => $email, 'descr' => $description ]; } } //print_vars($userlist); } } function ldap_internal_paged_entries($filter, $attributes) { global $config, $ds; if ($config['auth_ldap_version'] >= 3 && PHP_VERSION_ID >= 70300) { // Use pagination for speedup fetch huge lists, there is new style, see: // https://www.php.net/manual/en/ldap.examples-controls.php (Example #5) $page_size = 200; $entries = []; $cookie = ''; do { $search = ldap_search( $ds, trim($config['auth_ldap_suffix'], ', '), $filter, $attributes, 0, 0, 0, LDAP_DEREF_NEVER, [['oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => [ 'size' => $page_size, 'cookie' => $cookie ]]] ); if (ldap_internal_is_valid($search)) { ldap_parse_result($ds, $search, $errcode, $matcheddn, $errmsg, $referrals, $controls); print_debug(ldap_error($ds)); $entries = array_merge($entries, ldap_get_entries($ds, $search)); if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) { // You need to pass the cookie from the last call to the next one $cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie']; } else { $cookie = ''; } } else { $cookie = ''; } // Empty cookie means last page } while (!empty($cookie)); } elseif ($config['auth_ldap_version'] >= 3 && function_exists('ldap_control_paged_result')) { // Use pagination for speedup fetch huge lists, pre 7.3 style $page_size = 200; $entries = []; $cookie = ''; do { // WARNING, do not make any ldap queries between ldap_control_paged_result() and ldap_control_paged_result_response()!! // this produce loop and errors in queries $page_test = ldap_control_paged_result($ds, $page_size, TRUE, $cookie); //print_vars($page_test); print_debug(ldap_error($ds)); $search = ldap_search($ds, trim($config['auth_ldap_suffix'], ', '), $filter, $attributes); print_debug(ldap_error($ds)); if (ldap_internal_is_valid($search)) { $entries = array_merge($entries, ldap_get_entries($ds, $search)); //print_vars($filter); //print_vars($search); //ldap_internal_user_entries($entries, $userlist); ldap_control_paged_result_response($ds, $search, $cookie); } else { $cookie = ''; } } while($page_test && $cookie !== NULL && $cookie != ''); // Reset LDAP paged result ldap_control_paged_result($ds, 1000); } else { // Old php < 5.4, trouble with limit 1000 entries, see: // http://stackoverflow.com/questions/24990243/ldap-search-not-returning-more-than-1000-user $search = ldap_search($ds, trim($config['auth_ldap_suffix'], ', '), $filter, $attributes); print_debug(ldap_error($ds)); if (ldap_internal_is_valid($search)) { $entries = ldap_get_entries($ds, $search); //print_vars($filter); //print_vars($search); //ldap_internal_user_entries($entries, $userlist); } } return $entries; } /** * Returns the textual SID for Active Directory * Private function for this LDAP module only * * Source: http://stackoverflow.com/questions/13130291/how-to-query-ldap-adfs-by-objectsid-in-php-or-any-language-really * * @param string Binary SID * @return string Textual SID */ function ldap_bin_to_str_sid($binsid) { $hex_sid = bin2hex($binsid); $rev = hexdec(substr($hex_sid, 0, 2)); $subcount = hexdec(substr($hex_sid, 2, 2)); $auth = hexdec(substr($hex_sid, 4, 12)); $result = "$rev-$auth"; for ($x = 0; $x < $subcount; $x++) { $subauth[$x] = hexdec(ldap_little_endian(substr($hex_sid, 16 + ($x * 8), 8))); $result .= "-" . $subauth[$x]; } // Cheat by tacking on the S- return 'S-' . $result; } /** * Convert a little-endian hex-number to one that 'hexdec' can convert. * Private function for this LDAP module only. * * Source: http://stackoverflow.com/questions/13130291/how-to-query-ldap-adfs-by-objectsid-in-php-or-any-language-really * * @param string $hex Hexadecimal number * @return string Converted hexadecimal number */ function ldap_little_endian($hex) { $result = ''; for ($x = strlen($hex) - 2; $x >= 0; $x = $x - 2) { $result .= substr($hex, $x, 2); } return $result; } /** * Bind with either the configured bind DN, the user's configured DN, or anonymously, depending on config. * Private function for this LDAP module only. * * @param string $username Bind username (optional) * @param string $password Bind password (optional) * * @return bool FALSE if bind succeeded, TRUE if not */ function ldap_bind_dn($username = "", $password = "") { global $config, $ds, $cache; print_debug("LDAP[Bind DN called]"); // Avoid binding multiple times on one resource, this upsets some LDAP servers. if (isset($cache['ldap']['bind_result'])) { return $cache['ldap']['bind_result']; } if ($config['auth_ldap_binddn']) { // Bind user/password print_debug("LDAP[Bind][" . $config['auth_ldap_binddn'] . "]"); $bind = ldap_bind($ds, $config['auth_ldap_binddn'], $config['auth_ldap_bindpw']); } elseif ($config['auth_ldap_bindanonymous']) { // Try anonymous bind if configured to do so print_debug("LDAP[Bind][anonymous]"); $bind = ldap_bind($ds); } else { // Session bind if (($username == '' || $password == '') && isset($_SESSION['user_encpass'])) { // Use session credentials print_debug("LDAP[Bind][session]"); $username = $_SESSION['username']; if (!isset($_SESSION['encrypt_required'])) { $key = session_unique_id(); if (OBS_ENCRYPT_MODULE === 'mcrypt') { $key .= get_unique_id(); } $password = decrypt($_SESSION['user_encpass'], $key); } else { // WARNING, requires mcrypt or sodium $password = base64_decode($_SESSION['user_encpass'], TRUE); } } print_debug("LDAP[Bind][" . $config['auth_ldap_prefix'] . $username . $config['auth_ldap_suffix'] . "]"); $bind = ldap_bind($ds, $config['auth_ldap_prefix'] . $username . $config['auth_ldap_suffix'], $password); } if ($bind) { $cache['ldap']['bind_result'] = 0; return FALSE; } else { $cache['ldap']['bind_result'] = 1; print_debug("Error binding to LDAP server: " . implode(' ',$config['auth_ldap_server']) . ': ' . ldap_error($ds)); session_logout(); return TRUE; } } /** * Find user's Distinguished Name based on their username. * * Private function for this LDAP module only. * * @param string Username to retrieve DN for * * @return string User's Distinguished Name */ function ldap_internal_dn_from_username($username) { //r(debug_backtrace()); global $config, $ds, $cache; if (!isset($cache['ldap']['dn'][$username])) { ldap_init(); //ldap_bind_dn(); //$filter = "(" . $config['auth_ldap_attr']['uid'] . '=' . $username . ")"; $filter_params[] = ldap_filter_create('objectClass', $config['auth_ldap_objectclass']); $filter_params[] = ldap_filter_create($config['auth_ldap_attr']['uid'], $username); $filter = ldap_filter_combine($filter_params); print_debug("LDAP[Filter][$filter][" . trim($config['auth_ldap_suffix'], ', ') . "]"); $search = ldap_search($ds, trim($config['auth_ldap_suffix'], ', '), $filter); //r($search); //r(ldap_get_entries($ds, $search)); if (ldap_internal_is_valid($search)) { $entries = ldap_get_entries($ds, $search); if ($entries['count']) { list($cache['ldap']['dn'][$username],) = ldap_escape_filter_value($entries[0]['dn']); } } else { return ''; } } return $cache['ldap']['dn'][$username]; } /** * Calculate User's numeric ID from LDAP. * Fetches UID (through configured attribute) from the LDAP search result, with one caveat: * There is some special handling if uid attribute is objectSID; we grab the last numeric part * and hope it's unique. There is no other way to have a numeric ID from Active Directory - it is * highly recommended to use RFC2307 (unix attributes) in your AD forest, specifying a specific * POSIX-style "uid" for your users, so we can treat that as numeric user ID. * * Private function for this LDAP module only. * * @param object LDAP search result for the user * * @return int User ID. */ function ldap_internal_auth_user_id($result) { global $config; // For AD, convert SID S-1-5-21-4113566099-323201010-15454308-1104 to 1104 as our numeric unique ID if ($config['auth_ldap_attr']['uidNumber'] === "objectSid") { $sid = explode('-', ldap_bin_to_str_sid($result['objectsid'][0])); $userid = $sid[count($sid)-1]; print_debug("LDAP[UserID][Converted objectSid " . ldap_bin_to_str_sid($result['objectsid'][0]) . " to user ID " . $userid . "]"); } else { $userid = $result[strtolower($config['auth_ldap_attr']['uidNumber'])][0]; print_debug("LDAP[UserID][Attribute " . $config['auth_ldap_attr']['uidNumber'] . " yields user ID " . $userid . "]"); } if (!is_numeric($userid)) // FIXME, do this configurable? $config['auth_ldap_uid_number_generate'] = TRUE|FALSE; { $userid = string_to_id('ldap|' . $result[strtolower($config['auth_ldap_attr']['uid'])][0]); } return $userid; } /** * Compare value of attribute found in entry specified with DN * Internal implementation with workaround for dumb services. * * @param $ds * @param $dn * @param $attribute * @param $value * * @return bool */ function ldap_internal_compare($ds, $dn, $attribute, $value) { global $cache; // Return cached $cache_key = $dn . ',' . $attribute . '=' . $value; if (isset($cache['ldap']['compare'][$cache_key])) { return $cache['ldap']['compare'][$cache_key]; } $compare = ldap_compare($ds, $dn, $attribute, $value); //$compare = -1; // On error, try compare by get entries for some dumb services // https://jira.observium.org/browse/OBS-3611 if ($compare === -1) { $filter_params = [ ldap_filter_create($attribute, $value) ]; $filter = ldap_filter_combine($filter_params); if ($read = ldap_read($ds, $dn, $filter, [ 'dn', 'count' ], 1)) { $entry = ldap_get_entries($ds, $read); //print_vars($filter); //print_vars($dn); //print_vars($entry); $compare = (int)$entry['count'] === 1; } } // Cache $cache['ldap']['compare'][$cache_key] = $compare; return $compare; } /** * Retrieves list of domain controllers from DNS through SRV records. * Private function for this LDAP module only. * * @param string Domain name (fqdn-style) for the AD domain. * * @return array Array of server names to be used for LDAP. */ function ldap_domain_servers_from_dns($domain) { global $config; $servers = array(); $resolver = new Net_DNS2_Resolver(); $response = $resolver->query("_ldap._tcp.dc._msdcs.$domain", 'SRV', 'IN'); if ($response) { foreach ($response->answer as $answer) { $servers[] = $answer->target; } } return $servers; } /** * Constructor of a new part of a LDAP filter. * * Example: * ldap_filter_create('memberOf', 'name', '=') >>> '(memberOf=name)' * * @param string $param Name of the attribute the filter should apply to * @param string $value Filter value * @param string $condition Matching rule * @param boolean $escape Should $value be escaped? (default: yes) * @return string Generated filter */ function ldap_filter_create($param, $value, $condition = '=', $escape = TRUE) { if ($escape) { $value = ldap_escape_filter_value($value); $value = array_shift($value); } // Convert common rule name to ldap rule // Default rule is equals $condition = strtolower(trim($condition)); switch ($condition) { case 'ge': case '>=': $filter = '(' . $param . '>=' . $value . ')'; break; case 'le': case '<=': $filter = '(' . $param . '<=' . $value . ')'; break; case 'gt': case 'greater': case '>': $filter = '(' . $param . '>' . $value . ')'; break; case 'lt': case 'less': case '<': $filter = '(' . $param . '<' . $value . ')'; break; case 'match': case 'matches': case '~=': $filter = '(' . $param . '~=' . $value . ')'; break; case 'notmatches': case 'notmatch': case '!match': case '!~=': $filter = '(!(' . $param . '~=' . $value . '))'; break; case 'notequals': case 'isnot': case 'ne': case '!=': case '!': $filter = '(!(' . $param . '=' . $value . '))'; break; case 'equals': case 'eq': case 'is': case '==': case '=': default: $filter = '(' . $param . '=' . $value . ')'; } return $filter; } /** * Combine two or more filter objects using a logical operator * * @param array $values Array with Filter entries generated by ldap_filter_create() * @param string $condition The logical operator. May be "and", "or", "not" or the subsequent logical equivalents "&", "|", "!" * @return string Generated filter */ function ldap_filter_combine($values = array(), $condition = '&') { $count = count($values); if (!$count) { return ''; } $condition = strtolower(trim($condition)); switch ($condition) { case '!': case 'not': $filter = '(!'.implode('', $values).')'; break; case '|': case 'or': if ($count === 1) { $filter = array_shift($values); } else { $filter = '(|'.implode('', $values).')'; } break; case '&': case 'and': default: if ($count === 1) { $filter = array_shift($values); } else { $filter = '(&'.implode('', $values).')'; } } return $filter; } /** * Escapes the given VALUES according to RFC 2254 so that they can be safely used in LDAP filters. * * Any control characters with an ACII code < 32 as well as the characters with special meaning in * LDAP filters "*", "(", ")", and "\" (the backslash) are converted into the representation of a * backslash followed by two hex digits representing the hexadecimal value of the character. * * @param array|string $values Array of values to escape * * @return array Array $values, but escaped */ function ldap_escape_filter_value($values = array()) { // Parameter validation if (!is_array($values)) { $values = array($values); } foreach ($values as $key => $val) { // Escaping of filter meta characters $val = str_replace([ '\\', '\5c,', '*', '(', ')' ], [ '\5c', '\2c', '\2a', '\28', '\29' ], $val); // ASCII < 32 escaping $val = asc2hex32($val); if (NULL === $val) { $val = '\0'; } // apply escaped "null" if string is empty $values[$key] = $val; } return $values; } /** * Undoes the conversion done by {@link ldap_escape_filter_value()}. * * Converts any sequences of a backslash followed by two hex digits into the corresponding character. * * @param array $values Array of values to escape * * @return array Array $values, but unescaped */ function ldap_unescape_filter_value($values = array()) { // Parameter validation if (!is_array($values)) { $values = array($values); } foreach ($values as $key => $value) { // Translate hex code into ascii $values[$key] = hex2asc($value); } return $values; } function ldap_internal_is_valid($obj) { if (PHP_VERSION_ID >= 80100) { // ldap_bind() returns an LDAP\Connection instance in 8.1; previously, a resource was returned // ldap_search() returns an LDAP\Result instance in 8.1; previously, a resource was returned. return is_object($obj); } return is_resource($obj); } /** * Converts all ASCII chars < 32 to "\HEX" * * @param string $string String to convert * * @return string */ function asc2hex32($string) { for ($i = 0; $i < strlen($string); $i++) { $char = substr($string, $i, 1); if (ord($char) < 32) { $hex = dechex(ord($char)); if (strlen($hex) == 1) { $hex = '0'.$hex; } $string = str_replace($char, '\\'.$hex, $string); } } return $string; } /** * Converts all Hex expressions ("\HEX") to their original ASCII characters * * @param string $string String to convert * * @author beni@php.net, heavily based on work from DavidSmith@byu.net * @return string */ function hex2asc($string) { return preg_replace_callback("/\\\([0-9A-Fa-f]{2})/", function($matches){ foreach($matches as $match){ return chr(hexdec($match)); } }, $string); } // EOF