* LDAP: implement optional group-filter

also some code cleanups and fixes
This commit is contained in:
ralf 2022-05-20 21:46:48 +02:00
parent f42a10deb7
commit ab427562b7
4 changed files with 119 additions and 71 deletions

View File

@ -32,14 +32,15 @@ use setup_cmd_ldap;
*
* A user is recognised by eGW, if he's in the user_context tree AND has the posixAccount object class AND
* matches the LDAP search filter specified in setup >> configuration.
* A group is recogniced by eGW, if it's in the group_context tree AND has the posixGroup object class.
* A group is recognised by eGW, if it's in the group_context tree AND has the posixGroup object class AND
* - if specified - matches the LDAP group filter.
* The group members are stored as memberuid's.
*
* The (positive) group-id's (gidnumber) of LDAP groups are mapped in this class to negative numeric
* account_id's to not conflict with the user-id's, as both share in eGW internaly the same numberspace!
* account_id's to not conflict with the user-id's, as both share in eGW internally the same numberspace!
*
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @author Ralf Becker <rb@egroupware.org>
* @license https://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @access internal only use the interface provided by the accounts class
*/
class Ldap
@ -51,7 +52,7 @@ class Ldap
/**
* resource with connection to the ldap server
*
* @var resource
* @var resource|object
*/
var $ds;
/**
@ -72,6 +73,12 @@ class Ldap
* @var string
*/
var $group_context;
/**
* Additional LDAP search filter for groups
*
* @var string
*/
var $group_filter;
/**
* total number of found entries from get_list method
*
@ -79,10 +86,8 @@ class Ldap
*/
var $total;
var $ldapServerInfo;
/**
* required classe for user and groups
* required object-classes for user and groups
*
* @var array
*/
@ -140,7 +145,7 @@ class Ldap
const CHANGE_ACCOUNT_LID = true;
/**
* does backend requires password to be set, before allowing to enable an account
* does backend require password to be set, before allowing to enable an account
*/
const REQUIRE_PASSWORD_FOR_ENABLE = false;
@ -153,13 +158,34 @@ class Ldap
{
$this->frontend = $frontend;
$this->ldap = Api\Ldap::factory(false, $this->frontend->config['ldap_host'],
$this->frontend->config['ldap_root_dn'],$this->frontend->config['ldap_root_pw']);
$this->ds = $this->ldap->ds;
$this->ds = $this->ldap_connection();
$this->user_context = $this->frontend->config['ldap_context'];
$this->account_filter = $this->frontend->config['ldap_search_filter'];
$this->group_context = $this->frontend->config['ldap_group_context'] ?: $this->frontend->config['ldap_context'];
$this->group_filter = $this->frontend->config['ldap_group_filter'];
if (!empty($this->group_filter) && !($this->group_filter[0] === '(' && substr($this->group_filter, -1) === ')'))
{
$this->group_filter = '('.$this->group_filter.')';
}
}
/**
* Get connection to ldap server and optionally reconnect
*
* @param boolean $reconnect =false true: reconnect even if already connected
* @return resource|object
* @throws Api\Exception\AssertionFailed
* @throws Api\Exception\NoPermission
*/
function ldap_connection(bool $reconnect = false)
{
$this->ldap = Api\Ldap::factory(false, $this->frontend->config['ldap_host'],
$this->frontend->config['ldap_root_dn'],$this->frontend->config['ldap_root_pw'], $reconnect);
$this->serverinfo = $this->ldap->getLDAPServerInfo();
return $this->ldap->ds;
}
/**
@ -194,9 +220,9 @@ class Ldap
$data_utf8 = Api\Translation::convert($data,Api\Translation::charset(),'utf-8');
$members = $data['account_members'];
if (!is_object($this->ldapServerInfo))
if (!is_object($this->serverinfo))
{
$this->ldapServerInfo = $this->ldap->getLDAPServerInfo($this->frontend->config['ldap_host']);
$this->serverinfo = $this->ldap->getLDAPServerInfo();
}
// common code for users and groups
// checks if account_lid (dn) has been changed or required objectclass'es are missing
@ -264,7 +290,7 @@ class Ldap
$add = $additional;
$additional = array_shift($add);
}
if ($this->ldapServerInfo->supportsObjectClass($additional))
if ($this->serverinfo->supportsObjectClass($additional))
{
$to_write['objectclass'][] = $additional;
if ($add) $to_write += $add;
@ -299,7 +325,7 @@ class Ldap
$keep_objectclass = false;
if (is_array($forward)) list($forward,$extra_attr,$keep_objectclass) = $forward;
if ($this->ldapServerInfo->supportsObjectClass($objectclass) &&
if ($this->serverinfo->supportsObjectClass($objectclass) &&
($old && in_array($objectclass,$old['objectclass']) || $data_utf8['account_email'] || $old[static::MAIL_ATTR]))
{
if ($data_utf8['account_email']) // setting an email
@ -431,13 +457,13 @@ class Ldap
protected function _read_group($account_id)
{
$group = array();
if (!is_object($this->ldapServerInfo))
if (!is_object($this->serverinfo))
{
$this->ldapServerInfo = $this->ldap->getLDAPServerInfo($this->frontend->config['ldap_host']);
$this->serverinfo = $this->ldap->getLDAPServerInfo($this->frontend->config['ldap_host']);
}
foreach(array_keys($this->group_mail_classes) as $objectclass)
{
if ($this->ldapServerInfo->supportsObjectClass($objectclass))
if ($this->serverinfo->supportsObjectClass($objectclass))
{
$group['mailAllowed'] = $objectclass;
break;
@ -703,17 +729,17 @@ class Ldap
$filter = "(&(objectclass=posixaccount)";
if (!empty($query) && $query != '*')
{
switch($param['query_type'])
switch ($param['query_type'])
{
case 'all':
default:
$query = '*'.$query;
// fall-through
$query = '*' . $query;
// fall-through
case 'start':
$query .= '*';
// use now exact, as otherwise groups have "**pattern**", which dont match anything
$param['query_type'] = 'exact';
// fall-through
// fall-through
case 'exact':
$filter .= "(|(uid=$query)(sn=$query)(cn=$query)(givenname=$query)(mail=$query))";
break;
@ -723,11 +749,11 @@ class Ldap
case 'email':
$to_ldap = array(
'firstname' => 'givenname',
'lastname' => 'sn',
'lid' => 'uid',
'email' => static::MAIL_ATTR,
'lastname' => 'sn',
'lid' => 'uid',
'email' => static::MAIL_ATTR,
);
$filter .= '('.$to_ldap[$param['query_type']].'=*'.$query.'*)';
$filter .= '(' . $to_ldap[$param['query_type']] . '=*' . $query . '*)';
break;
}
}
@ -755,14 +781,15 @@ class Ldap
$order = isset($propertyMap[$orders[0]]) ? $propertyMap[$orders[0]] : 'uid';
$sri = ldap_search($this->ds, $this->user_context, $filter,array('uid', $order));
$fullSet = array();
foreach ((array)ldap_get_entries($this->ds, $sri) as $key => $entry)
foreach (ldap_get_entries($this->ds, $sri) ?: [] as $key => $entry)
{
if ($key !== 'count') $fullSet[$entry['uid'][0]] = $entry[$order][0];
}
if (is_numeric($param['type'])) // return only group-members
{
$sri = ldap_search($this->ds,$this->group_context,"(&(objectClass=posixGroup)(gidnumber=" . abs($param['type']) . "))",array('memberuid'));
$sri = ldap_search($this->ds,$this->group_context,"(&(objectClass=posixGroup)(gidnumber=" .
abs($param['type']) . "))",array('memberuid'));
$group = ldap_get_entries($this->ds, $sri);
$fullSet = $group[0]['memberuid'] ? array_intersect_key($fullSet, array_flip($group[0]['memberuid'])) : array();
}
@ -774,12 +801,12 @@ class Ldap
$filter = '(&(objectclass=posixaccount)(|(uid='.implode(')(uid=',$relevantAccounts).'))' . $this->account_filter.')';
$filter = str_replace(array('%user','%domain'),array('*',$GLOBALS['egw_info']['user']['domain']),$filter);
}
/** @noinspection SuspiciousAssignmentsInspection */
$sri = ldap_search($this->ds, $this->user_context, $filter,array('uid','uidNumber','givenname','sn',static::MAIL_ATTR,'shadowExpire','createtimestamp','modifytimestamp','objectclass','gidNumber'));
$utc_diff = date('Z');
foreach(ldap_get_entries($this->ds, $sri) as $allVals)
foreach(ldap_get_entries($this->ds, $sri) ?: [] as $allVals)
{
settype($allVals,'array');
$test = @$allVals['uid'][0];
if (!$this->frontend->config['global_denied_users'][$test] && $allVals['uid'][0])
{
@ -816,9 +843,9 @@ class Ldap
}
if ($param['type'] == 'groups' || $param['type'] == 'both')
{
if(empty($query) || $query == '*')
if(empty($query) || $query === '*')
{
$filter = '(objectclass=posixgroup)';
$filter = "(&(objectclass=posixgroup)$this->group_filter)";
}
else
{
@ -834,12 +861,11 @@ class Ldap
case 'exact':
break;
}
$filter = "(&(objectclass=posixgroup)(cn=$query))";
$filter = "(&(objectclass=posixgroup)(cn=$query)$this->group_filter)";
}
$sri = ldap_search($this->ds, $this->group_context, $filter,array('cn','gidNumber'));
foreach((array)ldap_get_entries($this->ds, $sri) as $allVals)
foreach(ldap_get_entries($this->ds, $sri) ?: [] as $allVals)
{
settype($allVals,'array');
$test = $allVals['cn'][0];
if (!$this->frontend->config['global_denied_groups'][$test] && $allVals['cn'][0])
{
@ -960,10 +986,10 @@ class Ldap
if (in_array($which, array('account_lid','account_email')) && $account_type !== 'u') // groups only support account_(lid|email)
{
$attr = $which == 'account_lid' ? 'cn' : static::MAIL_ATTR;
$sri = ldap_search($this->ds, $this->group_context, '(&('.$attr.'=' . $name . ')(objectclass=posixgroup))', array('gidNumber'));
$allValues = ldap_get_entries($this->ds, $sri);
if (@$allValues[0]['gidnumber'][0])
if (($sri = ldap_search($this->ds, $this->group_context, '(&('.$attr.'=' . $name . ")(objectclass=posixgroup)$this->group_filter)", array('gidNumber'))) &&
($allValues = ldap_get_entries($this->ds, $sri)) &&
!empty($allValues[0]['gidnumber'][0]))
{
return -$allValues[0]['gidnumber'][0];
}
@ -978,11 +1004,9 @@ class Ldap
return False;
}
$sri = ldap_search($this->ds, $this->user_context, '(&('.$to_ldap[$which].'=' . $name . ')(objectclass=posixaccount))', array('uidNumber'));
$allValues = ldap_get_entries($this->ds, $sri);
if (@$allValues[0]['uidnumber'][0])
if (($sri = ldap_search($this->ds, $this->user_context, '(&('.$to_ldap[$which].'=' . $name . ')(objectclass=posixaccount))', array('uidNumber'))) &&
($allValues = ldap_get_entries($this->ds, $sri)) &&
!empty($allValues[0]['uidnumber'][0]))
{
return (int)$allValues[0]['uidnumber'][0];
}
@ -1042,7 +1066,7 @@ class Ldap
*
* @param int $_gid
* @return array|boolean array with uidnumber => uid pairs,
* false if $_git is not nummeric and can't be resolved to a nummeric gid
* false if $_gid is not numeric and can't be resolved to a numeric gid
*/
function members($_gid)
{
@ -1121,7 +1145,7 @@ class Ldap
if (!isset($objectclass))
{
$objectclass = $this->id2name($gid, 'objectclass');
// if we cant find objectclass, we might ge in the middle of a migration
// if we can't find objectclass, we might ge in the middle of a migration
if (!isset($objectclass))
{
Api\Accounts::cache_invalidate($gid);
@ -1141,7 +1165,7 @@ class Ldap
$member_dn = $this->id2name($member, 'account_dn');
if (is_numeric($member)) $member = $this->id2name($member);
// only add a member, if we have the neccessary info / he already exists in migration
// only add a member, if we have the necessary info / he already exists in migration
if ($member && ($member_dn || !array_intersect(array('groupofnames','groupofuniquenames','univentiongroup'), $objectclass)))
{
$to_write['memberuid'][] = $member;
@ -1281,7 +1305,7 @@ class Ldap
return -1;
}
$id = (int)$GLOBALS['egw_info']['server'][$key='last_id_'.$location];
$id = (int)$GLOBALS['egw_info']['server']['last_id_'.$location];
if (!$id || $id < $min)
{
@ -1306,7 +1330,6 @@ class Ldap
{
$vars = get_object_vars($this);
unset($vars['ds']);
unset($this->ds);
return array_keys($vars);
}
@ -1315,7 +1338,6 @@ class Ldap
*/
function __wakeup()
{
$this->ds = Api\Ldap::factory(true, $this->frontend->config['ldap_host'],
$this->frontend->config['ldap_root_dn'],$this->frontend->config['ldap_root_pw']);
$this->ds = $this->ldap_connection();
}
}

View File

@ -26,7 +26,6 @@ use EGroupware\Api\Ldap\ServerInfo;
*/
class Ldap
{
const ALL = 0;
const ACCOUNTS = 1;
const PERSONAL = 2;
@ -372,7 +371,6 @@ class Ldap
{
$vars = get_object_vars($this);
unset($vars['ds']);
unset($this->ds);
return array_keys($vars);
}
@ -472,7 +470,7 @@ class Ldap
* reads contact data
*
* @param string|array $contact_id contact_id or array with values for id or account_id
* @return array/boolean data if row could be retrived else False
* @return array|false data if row could be retrived else False
*/
function read($contact_id)
{
@ -512,6 +510,7 @@ class Ldap
*
* @param array $keys if given $keys are copied to data before saveing => allows a save as
* @return int 0 on success and errno != 0 else
* @noinspection UnsupportedStringOffsetOperationsInspection
*/
function save($keys=null)
{
@ -785,7 +784,7 @@ class Ldap
* @param string $join ='' sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or
* "LEFT JOIN table2 ON (x=y)", Note: there's no quoting done on $join!
* @param boolean $need_full_no_count =false If true an unlimited query is run to determine the total number of rows, default false
* @return array of matching rows (the row is an array of the cols) or False
* @return array|false of matching rows (the row is an array of the cols) or False
*/
function &search($criteria,$only_keys=True,$order_by='',$extra_cols='',$wildcard='',$empty=False,$op='AND',$start=false,$filter=null,$join='',$need_full_no_count=false)
{
@ -1024,9 +1023,9 @@ class Ldap
elseif ($value)
{
if (is_array($value)) $filters .= '(|';
foreach((array)$value as $value)
foreach((array)$value as $val)
{
$filters .= '(uidNumber='.(int)$value.')';
$filters .= '(uidNumber='.(int)$val.')';
}
if (is_array($value)) $filters .= ')';
}
@ -1192,7 +1191,7 @@ class Ldap
* @param int $_addressbooktype
* @param array $_skipPlugins =null schema-plugins to skip
* @param string $order_by sql order string eg. "contact_email ASC"
* @param null|int|array $start [$start,$offset], on return null, if result sorted and limited by server
* @param null|int|array $start [$start, $num_rows], on return null, if result sorted and limited by server
* @return array/boolean with eGW contacts or false on error
*/
function _searchLDAP($_ldapContext, $_filter, $_attributes, $_addressbooktype, array $_skipPlugins=null, $order_by=null, &$start=null)

View File

@ -20,7 +20,7 @@ namespace EGroupware\Api;
* Please note for SSL or TLS connections hostname has to be:
* - SSL: "ldaps://host[:port]/"
* - TLS: "tls://host[:port]/"
* Both require certificats installed on the webserver, otherwise the connection will fail!
* Both require certificates installed on the webserver, otherwise the connection will fail!
*
* If multiple (space-separated) ldap hosts or urls are given, try them in order and
* move first successful one to first place in session, to try not working ones
@ -79,15 +79,16 @@ class Ldap
* @param string $host ='' ldap host, default $GLOBALS['egw_info']['server']['ldap_host']
* @param string $dn ='' ldap dn, default $GLOBALS['egw_info']['server']['ldap_root_dn']
* @param string $passwd ='' ldap pw, default $GLOBALS['egw_info']['server']['ldap_root_pw']
* @param bool $reconnect default false, true: reconnect, even if we have an existing connection
* @return object|resource|self|false resource/object from ldap_connect(), self or false on error
* @throws Exception\AssertionFailed 'LDAP support unavailable!' (no ldap extension)
* @throws Exception\NoPermission if bind fails
*/
public static function factory($ressource=true, $host='', $dn='', $passwd='')
public static function factory($ressource=true, $host='', $dn='', $passwd='', bool $reconnect=false)
{
$key = md5($host.':'.$dn.':'.$passwd);
if (!isset(self::$connections[$key]))
if (!isset(self::$connections[$key]) || $reconnect)
{
self::$connections[$key] = new Ldap(true);
@ -302,4 +303,27 @@ class Ldap
Cache::setSession(__CLASS__, 'ldapServerInfo', $this->ldapserverinfo);
}
}
/**
* Magic method called when object gets serialized
*
* We do NOT store ldapConnection, as we need to reconnect anyway.
* PHP 8.1 gives an error when trying to serialize LDAP\Connection object!
*
* @return array
*/
function __sleep()
{
$vars = get_object_vars($this);
unset($vars['ds']);
return array_keys($vars);
}
/**
* __wakeup function gets called by php while unserializing the object to reconnect with the ldap server
*/
function __wakeup()
{
}
}

View File

@ -308,16 +308,21 @@
</tr>
<tr class="row_on">
<td>{lang_Additional_group_filter_(optional)}:</td>
<td><input name="newsettings[ldap_group_filter]" value="{value_ldap_group_filter}" size="40" /></td>
</tr>
<tr class="row_off">
<td>{lang_LDAP_rootdn} {lang_(searching_accounts_and_changing_passwords)}:</td>
<td><input name="newsettings[ldap_root_dn]" value="{value_ldap_root_dn}" size="40" /></td>
</tr>
<tr class="row_off">
<tr class="row_on">
<td>{lang_LDAP_root_password}:</td>
<td><input name="newsettings[ldap_root_pw]" type="password" value="{value_ldap_root_pw}" /></td>
</tr>
<tr class="row_on">
<tr class="row_off">
<td>{lang_LDAP_encryption_type}:</td>
<td>
<select name="newsettings[ldap_encryption_type]">
@ -326,7 +331,7 @@
</td>
</tr>
<tr class="row_off">
<tr class="row_on">
<td>{lang_Do_you_want_to_manage_homedirectory_and_loginshell_attributes?}:</td>
<td>
<select name="newsettings[ldap_extra_attributes]">
@ -336,17 +341,17 @@
</td>
</tr>
<tr class="row_on">
<tr class="row_off">
<td>{lang_LDAP_Default_homedirectory_prefix_(e.g._/home_for_/home/username)}:</td>
<td><input name="newsettings[ldap_account_home]" value="{value_ldap_account_home}" /></td>
</tr>
<tr class="row_off">
<tr class="row_on">
<td>{lang_LDAP_Default_shell_(e.g._/bin/bash)}:</td>
<td><input name="newsettings[ldap_account_shell]" value="{value_ldap_account_shell}" /></td>
</tr>
<tr class="row_on">
<tr class="row_off">
<td>{lang_Allow_usernames_identical_to_system_users?}:</td>
<td>
<select name="newsettings[ldap_allow_systemusernames]">
@ -356,7 +361,7 @@
</td>
</tr>
<tr class="row_off" valign="top">
<tr class="row_on" valign="top">
<td colspan="2">
<a href="account_migration.php"><b>{lang_Migration_between_eGroupWare_account_repositories}:</b></a>
</td>
@ -709,6 +714,4 @@
</tr>
</table>
</form>
<!-- END footer -->
<!-- END footer -->