From 1a0dd6214e7836451e495ece37885f9e6dc8a8a6 Mon Sep 17 00:00:00 2001 From: ralf Date: Fri, 20 May 2022 21:46:48 +0200 Subject: [PATCH] * LDAP: implement optional group-filter also some code cleanups and fixes --- api/src/Accounts/Ldap.php | 133 ++++++++++++++++------------- api/src/Contacts/Ldap.php | 13 ++- api/src/Ldap.php | 30 ++++++- setup/templates/default/config.tpl | 23 ++--- 4 files changed, 121 insertions(+), 78 deletions(-) diff --git a/api/src/Accounts/Ldap.php b/api/src/Accounts/Ldap.php index fa3b8bab09..6b64128a9f 100644 --- a/api/src/Accounts/Ldap.php +++ b/api/src/Accounts/Ldap.php @@ -32,14 +32,15 @@ use setup_cmd_ldap; * * A user is recogniced 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 - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @author Ralf Becker + * @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,17 +158,34 @@ class Ldap { $this->frontend = $frontend; - // enable the caching in the session, done by the accounts class extending this class. - $this->use_session_cache = true; - - $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_group_context'] : $this->frontend->config['ldap_context']; + $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; } /** @@ -198,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 accout_lid (dn) has been changed or required objectclass'es are missing @@ -268,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; @@ -303,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 @@ -435,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; @@ -707,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; @@ -727,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; } } @@ -759,15 +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 { - $relevantAccounts = array(); - $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(); } @@ -779,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]) { @@ -821,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 { @@ -839,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]) { @@ -965,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]; } @@ -983,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]; } @@ -1047,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) { @@ -1126,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); @@ -1146,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; @@ -1286,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) { @@ -1311,7 +1330,6 @@ class Ldap { $vars = get_object_vars($this); unset($vars['ds']); - unset($this->ds); return array_keys($vars); } @@ -1320,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(); } -} \ No newline at end of file +} diff --git a/api/src/Contacts/Ldap.php b/api/src/Contacts/Ldap.php index af40afb50c..4cf04f981b 100644 --- a/api/src/Contacts/Ldap.php +++ b/api/src/Contacts/Ldap.php @@ -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) diff --git a/api/src/Ldap.php b/api/src/Ldap.php index b30d3a79f5..4c71958eaa 100644 --- a/api/src/Ldap.php +++ b/api/src/Ldap.php @@ -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() + { + + } } \ No newline at end of file diff --git a/setup/templates/default/config.tpl b/setup/templates/default/config.tpl index 3e4293b11f..6c1266163b 100644 --- a/setup/templates/default/config.tpl +++ b/setup/templates/default/config.tpl @@ -308,16 +308,21 @@ + {lang_Additional_group_filter_(optional)}: + + + + {lang_LDAP_rootdn} {lang_(searching_accounts_and_changing_passwords)}: - + {lang_LDAP_root_password}: - + {lang_LDAP_encryption_type}: @@ -336,17 +341,17 @@ - + {lang_LDAP_Default_homedirectory_prefix_(e.g._/home_for_/home/username)}: - + {lang_LDAP_Default_shell_(e.g._/bin/bash)}: - + {lang_Allow_usernames_identical_to_system_users?}: