1172 lines
37 KiB
PHP
1172 lines
37 KiB
PHP
<?php
|
|
/**
|
|
* Observium
|
|
*
|
|
* This file is part of Observium.
|
|
*
|
|
* @package observium
|
|
* @subpackage authentication
|
|
* @copyright (C) 2006-2013 Adam Armstrong, (C) 2013-2022 Observium Limited
|
|
*
|
|
*/
|
|
|
|
// Warn if authentication will be impossible.
|
|
check_extension_exists('ldap', 'LDAP selected as authentication module, but PHP does not have LDAP support! Please load the PHP LDAP module.', TRUE);
|
|
|
|
// Set LDAP debugging level to 7 (dumped to Apache daemon error log) (not virtualhost error log!)
|
|
if (defined('OBS_DEBUG') && OBS_DEBUG > 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
|