diff --git a/plugins/kolab_delegation/kolab_delegation_engine.php b/plugins/kolab_delegation/kolab_delegation_engine.php index bb6a71b8..b5ad4a6c 100644 --- a/plugins/kolab_delegation/kolab_delegation_engine.php +++ b/plugins/kolab_delegation/kolab_delegation_engine.php @@ -1,968 +1,968 @@ <?php /** * Kolab Delegation Engine * * @version @package_version@ * @author Thomas Bruederli <bruederli@kolabsys.com> * @author Aleksander Machniak <machniak@kolabsys.com> * * Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ class kolab_delegation_engine { public $context; private $rc; private $ldap; private $ldap_filter; private $ldap_delegate_field; private $ldap_login_field; private $ldap_name_field; private $ldap_email_field; private $ldap_org_field; private $ldap_dn; private $cache = array(); private $folder_types = array('mail', 'event', 'task'); const ACL_READ = 1; const ACL_WRITE = 2; /** * Class constructor */ public function __construct() { $this->rc = rcube::get_instance(); } /** * Add delegate * * @param string|array $delegate Delegate DN (encoded) or delegate data (result of delegate_get()) * @param array $acl List of folder->right map * * @return string On error returns an error label, on success returns null */ public function delegate_add($delegate, $acl) { if (!is_array($delegate)) { $delegate = $this->delegate_get($delegate); } $dn = $delegate['ID']; if (empty($delegate) || empty($dn)) { return 'createerror'; } $list = $this->list_delegates(); $list = array_keys((array)$list); $list = array_filter($list); if (in_array($dn, $list)) { return 'delegationexisterror'; } // add delegate to the list $list[] = $dn; $list = array_map(array('kolab_ldap', 'dn_decode'), $list); // update user record $result = $this->user_update_delegates($list); // Set ACL on folders if ($result && !empty($acl)) { $this->delegate_acl_update($delegate['uid'], $acl); } return $result ? null : 'createerror'; } /** * Set/Update ACL on delegator's folders * * @param string $uid Delegate authentication identifier * @param array $acl List of folder->right map * @param bool $update Update (remove) old rights * * @return string On error returns an error label, on success returns null */ public function delegate_acl_update($uid, $acl, $update = false) { $storage = $this->rc->get_storage(); $right_types = $this->right_types(); $folders = $update ? $this->list_folders($uid) : array(); foreach ($acl as $folder_name => $rights) { $r = $right_types[$rights]; if ($r) { $storage->set_acl($folder_name, $uid, $r); } else { $storage->delete_acl($folder_name, $uid); } if (!empty($folders) && isset($folders[$folder_name])) { unset($folders[$folder_name]); } } foreach ($folders as $folder_name => $folder) { if ($folder['rights']) { $storage->delete_acl($folder_name, $uid); } } } /** * Delete delgate * * @param string $dn Delegate DN (encoded) * @param bool $acl_del Enable ACL deletion on delegator folders * * @return string On error returns an error label, on success returns null */ public function delegate_delete($dn, $acl_del = false) { $delegate = $this->delegate_get($dn); $list = $this->list_delegates(); $user = $this->user(); if (empty($delegate) || !isset($list[$dn])) { return 'deleteerror'; } // remove delegate from the list unset($list[$dn]); $list = array_keys($list); $list = array_map(array('kolab_ldap', 'dn_decode'), $list); $user[$this->ldap_delegate_field] = $list; // update user record $result = $this->user_update_delegates($list); // remove ACL if ($result && $acl_del) { $this->delegate_acl_update($delegate['uid'], array(), true); } return $result ? null : 'deleteerror'; } /** * Return delegate data * * @param string $dn Delegate DN (encoded) * * @return array Delegate record (ID, name, uid, imap_uid) */ public function delegate_get($dn) { // use internal cache so we not query LDAP more than once per request if (!isset($this->cache[$dn])) { $ldap = $this->ldap(); if (!$ldap || empty($dn)) { return array(); } // Get delegate $user = $ldap->get_record(kolab_ldap::dn_decode($dn)); if (empty($user)) { return array(); } $delegate = $this->parse_ldap_record($user); $delegate['ID'] = $dn; $this->cache[$dn] = $delegate; } return $this->cache[$dn]; } /** * Return delegate data * * @param string $login Delegate name (the 'uid' returned in get_users()) * * @return array Delegate record (ID, name, uid, imap_uid) */ public function delegate_get_by_name($login) { $ldap = $this->ldap(); if (!$ldap || empty($login)) { return array(); } $list = $ldap->dosearch($this->ldap_login_field, $login, 1); if (count($list) == 1) { $dn = key($list); $user = $list[$dn]; return $this->parse_ldap_record($user, $dn); } } /** * LDAP object getter */ private function ldap() { if ($this->ldap !== null) { return $this->ldap; } $this->ldap = kolab_storage::ldap('kolab_delegation_addressbook'); if (!$this->ldap || !$this->ldap->ready) { return null; } // Default filter of LDAP queries $this->ldap_filter = $this->rc->config->get('kolab_delegation_filter', '(|(objectClass=kolabInetOrgPerson)(&(objectclass=kolabsharedfolder)(kolabFolderType=mail)))'); // Name of the LDAP field for delegates list $this->ldap_delegate_field = $this->rc->config->get('kolab_delegation_delegate_field', 'kolabDelegate'); // Encoded LDAP DN of current user, set on login by kolab_auth plugin $this->ldap_dn = $_SESSION['kolab_dn']; // Name of the LDAP field with authentication ID $this->ldap_login_field = $this->rc->config->get('kolab_delegation_login_field', $this->rc->config->get('kolab_auth_login')); // Name of the LDAP field with user name used for identities $this->ldap_name_field = $this->rc->config->get('kolab_delegation_name_field', $this->rc->config->get('kolab_auth_name')); // Name of the LDAP field with email addresses used for identities $this->ldap_email_field = $this->rc->config->get('kolab_delegation_email_field', $this->rc->config->get('kolab_auth_email')); // Name of the LDAP field with organization name for identities $this->ldap_org_field = $this->rc->config->get('kolab_delegation_organization_field', $this->rc->config->get('kolab_auth_organization')); $this->ldap->set_filter($this->ldap_filter); $this->ldap->extend_fieldmap(array($this->ldap_delegate_field => $this->ldap_delegate_field)); return $this->ldap; } /** * List current user delegates */ public function list_delegates() { $result = array(); $ldap = $this->ldap(); $user = $this->user(); if (empty($ldap) || empty($user)) { return array(); } // Get delegates of current user - $delegates = $user[$this->ldap_delegate_field]; + $delegates = $user[$this->ldap_delegate_field] ?? null; if (!empty($delegates)) { foreach ((array)$delegates as $dn) { $delegate = $ldap->get_record($dn); $data = $this->parse_ldap_record($delegate, $dn); if (!empty($data) && !empty($data['name'])) { $result[$data['ID']] = $data['name']; } } } return $result; } /** * List current user delegators * * @return array List of delegators */ public function list_delegators() { $result = array(); $ldap = $this->ldap(); if (empty($ldap) || empty($this->ldap_dn)) { return array(); } $list = $ldap->dosearch($this->ldap_delegate_field, $this->ldap_dn, 1); foreach ($list as $dn => $delegator) { $delegator = $this->parse_ldap_record($delegator, $dn); $result[$delegator['ID']] = $delegator; } return $result; } /** * List current user delegators in format compatible with Calendar plugin * * @return array List of delegators */ public function list_delegators_js() { $list = $this->list_delegators(); $result = array(); foreach ($list as $delegator) { $name = $delegator['name']; if ($pos = strrpos($name, '(')) { $name = trim(substr($name, 0, $pos)); } $result[$delegator['imap_uid']] = array( 'emails' => ';' . implode(';', $delegator['email']), 'email' => $delegator['email'][0], 'name' => $name, ); } return $result; } /** * Prepare namespace prefixes for JS environment * * @return array List of prefixes */ public function namespace_js() { $storage = $this->rc->get_storage(); $ns = $storage->get_namespace('other'); if ($ns) { foreach ($ns as $idx => $nsval) { $ns[$idx] = kolab_storage::folder_id($nsval[0]); } } return $ns; } /** * Get all folders to which current user has admin access * * @param string $delegate IMAP user identifier * * @return array Folder type/rights */ public function list_folders($delegate = null) { $storage = $this->rc->get_storage(); $folders = $storage->list_folders(); $metadata = kolab_storage::folders_typedata(); $result = array(); if (!is_array($metadata)) { return $result; } // Definition of read and write ACL $right_types = $this->right_types(); $delegate_lc = strtolower($delegate); foreach ($folders as $folder) { // get only folders in personal namespace if ($storage->folder_namespace($folder) != 'personal') { continue; } $rights = null; $type = $metadata[$folder] ?: 'mail'; list($class, $subclass) = explode('.', $type); if (!in_array($class, $this->folder_types)) { continue; } // in edit mode, get folder ACL if ($delegate) { // @TODO: cache ACL $imap_acl = $storage->get_acl($folder); if (!empty($imap_acl) && (($acl = $imap_acl[$delegate]) || ($acl = $imap_acl[$delegate_lc]))) { if ($this->acl_compare($acl, $right_types[self::ACL_WRITE])) { $rights = self::ACL_WRITE; } else if ($this->acl_compare($acl, $right_types[self::ACL_READ])) { $rights = self::ACL_READ; } } } else if ($folder == 'INBOX' || $subclass == 'default' || $subclass == 'inbox') { $rights = self::ACL_WRITE; } $result[$folder] = array( 'type' => $class, 'rights' => $rights, ); } return $result; } /** * Returns list of users for autocompletion * * @param string $search Search string * * @return array Users list */ public function list_users($search) { $ldap = $this->ldap(); if (empty($ldap) || $search === '' || $search === null) { return array(); } $max = (int) $this->rc->config->get('autocomplete_max', 15); $mode = (int) $this->rc->config->get('addressbook_search_mode'); $fields = array_unique(array_filter(array_merge((array)$this->ldap_name_field, (array)$this->ldap_login_field))); $users = array(); $keys = array(); $result = $ldap->dosearch($fields, $search, $mode, (array)$this->ldap_login_field, $max); foreach ($result as $record) { // skip self if ($record['dn'] == $_SESSION['kolab_dn']) { continue; } $user = $this->parse_ldap_record($record); if ($user['uid']) { $display = rcube_addressbook::compose_search_name($record); $user = array('name' => $user['uid'], 'display' => $display); $users[] = $user; $keys[] = $display ?: $user['uid']; } } if (count($users)) { // sort users index asort($keys, SORT_LOCALE_STRING); // re-sort users according to index foreach (array_keys($keys) as $idx) { $keys[$idx] = $users[$idx]; } $users = array_values($keys); } return $users; } /** * Extract delegate identifiers and pretty name from LDAP record */ private function parse_ldap_record($data, $dn = null) { $email = array(); $uid = $data[$this->ldap_login_field]; if (is_array($uid)) { $uid = array_filter($uid); $uid = $uid[0]; } // User name for identity foreach ((array)$this->ldap_name_field as $field) { $name = is_array($data[$field]) ? $data[$field][0] : $data[$field]; if (!empty($name)) { break; } } // User email(s) for identity foreach ((array)$this->ldap_email_field as $field) { $user_email = is_array($data[$field]) ? array_filter($data[$field]) : $data[$field]; if (!empty($user_email)) { $email = array_merge((array)$email, (array)$user_email); } } // Organization for identity foreach ((array)$this->ldap_org_field as $field) { $organization = is_array($data[$field]) ? $data[$field][0] : $data[$field]; if (!empty($organization)) { break; } } $realname = $name; if ($uid && $name) { $name .= ' (' . $uid . ')'; } else { $name = $uid; } // get IMAP uid - identifier used in shared folder hierarchy $imap_uid = $uid; if ($pos = strpos($imap_uid, '@')) { $imap_uid = substr($imap_uid, 0, $pos); } return array( 'ID' => kolab_ldap::dn_encode($dn), 'uid' => $uid, 'name' => $name, 'realname' => $realname, 'imap_uid' => $imap_uid, 'email' => $email, 'organization' => $organization, ); } /** * Returns LDAP record of current user * * @return array User data */ public function user($parsed = false) { if (!isset($this->cache['user'])) { $ldap = $this->ldap(); if (!$ldap) { return array(); } // Get current user record $this->cache['user'] = $ldap->get_record($this->ldap_dn); } return $parsed ? $this->parse_ldap_record($this->cache['user']) : $this->cache['user']; } /** * Update LDAP record of current user * * @param array List of delegates */ public function user_update_delegates($list) { $ldap = $this->ldap(); $pass = $this->rc->decrypt($_SESSION['password']); if (!$ldap) { return false; } // need to bind as self for sufficient privilages if (!$ldap->bind($this->ldap_dn, $pass)) { return false; } $user[$this->ldap_delegate_field] = $list; unset($this->cache['user']); // replace delegators list in user record return $ldap->replace($this->ldap_dn, $user); } /** * Manage delegation data on user login */ public function delegation_init() { // Fetch all delegators from LDAP who assigned the // current user as their delegate and create identities // a) if identity with delegator's email exists, continue // b) create identity ($delegate on behalf of $delegator // <$delegator-email>) for new delegators // c) remove all other identities which do not match the user's primary // or alias email if 'kolab_delegation_purge_identities' is set. $delegators = $this->list_delegators(); $use_subs = $this->rc->config->get('kolab_use_subscriptions'); $identities = $this->rc->user->list_emails(); $emails = array(); $uids = array(); if (!empty($delegators)) { $storage = $this->rc->get_storage(); $other_ns = $storage->get_namespace('other') ?: array(); $folders = $storage->list_folders(); } // convert identities to simpler format for faster access foreach ($identities as $idx => $ident) { // get user name from default identity if (!$idx) { $default = array( 'name' => $ident['name'], ); } $emails[$ident['identity_id']] = $ident['email']; } // for every delegator... foreach ($delegators as $delegator) { $uids[$delegator['imap_uid']] = $email_arr = $delegator['email']; $diff = array_intersect($emails, $email_arr); // identity with delegator's email already exist, do nothing if (count($diff)) { $emails = array_diff($emails, $email_arr); continue; } // create identities for delegator emails foreach ($email_arr as $email) { // @TODO: "Delegatorname" or "Username on behalf of Delegatorname"? $default['name'] = $delegator['realname']; $default['email'] = $email; // Database field for organization is NOT NULL $default['organization'] = empty($delegator['organization']) ? '' : $delegator['organization']; $this->rc->user->insert_identity($default); } // IMAP folders shared by new delegators shall be subscribed on login, // as well as existing subscriptions of previously shared folders shall // be removed. I suppose the latter one is already done in Roundcube. // for every accessible folder... foreach ($folders as $folder) { // for every 'other' namespace root... foreach ($other_ns as $ns) { $prefix = $ns[0] . $delegator['imap_uid']; // subscribe delegator's folder if ($folder === $prefix || strpos($folder, $prefix . substr($ns[0], -1)) === 0) { // Event/Task folders need client-side activation $type = kolab_storage::folder_type($folder); if (preg_match('/^(event|task)/i', $type)) { kolab_storage::folder_activate($folder); } // Subscribe to mail folders and (if system is configured // to display only subscribed folders) to other if ($use_subs || preg_match('/^mail/i', $type)) { $storage->subscribe($folder); } } } } } // remove identities that "do not belong" to user nor delegators if ($this->rc->config->get('kolab_delegation_purge_identities')) { $user = $this->user(true); $emails = array_diff($emails, $user['email']); foreach (array_keys($emails) as $idx) { $this->rc->user->delete_identity($idx); } } $_SESSION['delegators'] = $uids; } /** * Sets delegator context according to email message recipient * * @param rcube_message $message Email message object */ public function delegator_context_from_message($message) { if (empty($_SESSION['delegators'])) { return; } // Match delegators' addresses with message To: address // @TODO: Is this reliable enough? // Roundcube sends invitations to every attendee separately, // but maybe there's a software which sends with CC header or many addresses in To: $emails = $message->get_header('to'); $emails = rcube_mime::decode_address_list($emails, null, false); foreach ($emails as $email) { foreach ($_SESSION['delegators'] as $uid => $addresses) { if (in_array($email['mailto'], $addresses)) { return $this->context = $uid; } } } } /** * Return (set) current delegator context * * @return string Delegator UID */ public function delegator_context() { if (!$this->context && !empty($_SESSION['delegators'])) { $context = rcube_utils::get_input_value('_context', rcube_utils::INPUT_GPC); if ($context && isset($_SESSION['delegators'][$context])) { $this->context = $context; } } return $this->context; } /** * Set user identity according to delegator delegator * * @param array $args Reference to plugin hook arguments */ public function delegator_identity_filter(&$args) { $context = $this->delegator_context(); if (!$context) { return; } $identities = $this->rc->user->list_emails(); $emails = $_SESSION['delegators'][$context]; foreach ($identities as $ident) { if (in_array($ident['email'], $emails)) { $args['identity'] = $ident; return; } } // fallback to default identity $args['identity'] = array_shift($identities); } /** * Filter user emails according to delegator context * * @param array $args Reference to plugin hook arguments */ public function delegator_emails_filter(&$args) { $context = $this->delegator_context(); // try to derive context from the given user email if (!$context && !empty($args['emails'])) { if (($user = preg_replace('/@.+$/', '', $args['emails'][0])) && isset($_SESSION['delegators'][$user])) { $context = $user; } } // return delegator's addresses if ($context) { $args['emails'] = $_SESSION['delegators'][$context]; $args['abort'] = true; } // return only user addresses (exclude all delegators addresses) else if (!empty($_SESSION['delegators'])) { $identities = $this->rc->user->list_emails(); $emails[] = $this->rc->user->get_username(); foreach ($identities as $identity) { $emails[] = $identity['email']; } foreach ($_SESSION['delegators'] as $delegator_emails) { $emails = array_diff($emails, $delegator_emails); } $args['emails'] = array_unique($emails); $args['abort'] = true; } } /** * Filters list of calendar/task folders according to delegator context * * @param array $args Reference to plugin hook arguments */ public function delegator_folder_filter(&$args, $mode = 'calendars') { $context = $this->delegator_context(); if (empty($context)) { return $args; } $storage = $this->rc->get_storage(); $other_ns = $storage->get_namespace('other') ?: array(); $delim = $storage->get_hierarchy_delimiter(); if ($mode == 'calendars') { $editable = $args['filter'] & calendar_driver::FILTER_WRITEABLE; $active = $args['filter'] & calendar_driver::FILTER_ACTIVE; $personal = $args['filter'] & calendar_driver::FILTER_PERSONAL; $shared = $args['filter'] & calendar_driver::FILTER_SHARED; } else { $editable = $args['filter'] & tasklist_driver::FILTER_WRITEABLE; $active = $args['filter'] & tasklist_driver::FILTER_ACTIVE; $personal = $args['filter'] & tasklist_driver::FILTER_PERSONAL; $shared = $args['filter'] & tasklist_driver::FILTER_SHARED; } $folders = array(); foreach ($args['list'] as $folder) { if (isset($folder->ready) && !$folder->ready) { continue; } if ($editable && !$folder->editable) { continue; } if ($active && !$folder->storage->is_active()) { continue; } if ($personal || $shared) { $ns = $folder->get_namespace(); if ($personal && $ns == 'personal') { continue; } else if ($personal && $ns == 'other') { $found = false; foreach ($other_ns as $ns) { $c_folder = $ns[0] . $context . $delim; if (strpos($folder->name, $c_folder) === 0) { $found = true; } } if (!$found) { continue; } } else if (!$shared || $ns != 'shared') { continue; } } $folders[$folder->id] = $folder; } $args[$mode] = $folders; $args['abort'] = true; } /** * Filters/updates message headers according to delegator context * * @param array $args Reference to plugin hook arguments */ public function delegator_delivery_filter(&$args) { // no context, but message still can be send on behalf of... if (!empty($_SESSION['delegators'])) { $message = $args['message']; $headers = $message->headers(); // get email address from From: header $from = rcube_mime::decode_address_list($headers['From']); $from = array_shift($from); $from = $from['mailto']; foreach ($_SESSION['delegators'] as $uid => $addresses) { if (in_array($from, $addresses)) { $context = $uid; break; } } // add Sender: header with current user default identity if ($context) { $identity = $this->rc->user->get_identity(); $sender = format_email_recipient($identity['email'], $identity['name']); $message->headers(array('Sender' => $sender), false, true); } } } /** * Compares two ACLs (according to supported rights) * * @param array $acl1 ACL rights array (or string) * @param array $acl2 ACL rights array (or string) * * @param bool True if $acl1 contains all rights from $acl2 */ function acl_compare($acl1, $acl2) { if (!is_array($acl1)) $acl1 = str_split($acl1); if (!is_array($acl2)) $acl2 = str_split($acl2); $rights = $this->rights_supported(); $acl1 = array_intersect($acl1, $rights); $acl2 = array_intersect($acl2, $rights); $res = array_intersect($acl1, $acl2); $cnt1 = count($res); $cnt2 = count($acl2); if ($cnt1 >= $cnt2) { return true; } } /** * Get list of supported access rights (according to RIGHTS capability) * * @todo: this is stolen from acl plugin, move to rcube_storage/rcube_imap * * @return array List of supported access rights abbreviations */ public function rights_supported() { if ($this->supported !== null) { return $this->supported; } $storage = $this->rc->get_storage(); $capa = $storage->get_capability('RIGHTS'); if (is_array($capa)) { $rights = strtolower($capa[0]); } else { $rights = 'cd'; } return $this->supported = str_split('lrswi' . $rights . 'pa'); } private function right_types() { // Get supported rights and build column names $supported = $this->rights_supported(); // depending on server capability either use 'te' or 'd' for deleting msgs $deleteright = implode(array_intersect(str_split('ted'), $supported)); return array( self::ACL_READ => 'lrs', self::ACL_WRITE => 'lrswi'.$deleteright, ); } } diff --git a/plugins/libkolab/lib/kolab_ldap.php b/plugins/libkolab/lib/kolab_ldap.php index d7266f8c..c51fede4 100644 --- a/plugins/libkolab/lib/kolab_ldap.php +++ b/plugins/libkolab/lib/kolab_ldap.php @@ -1,613 +1,613 @@ <?php /** * Kolab Authentication and User Base * * @author Aleksander Machniak <machniak@kolabsys.com> * * Copyright (C) 2011-2019, Kolab Systems AG <contact@kolabsys.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /** * Wrapper class for rcube_ldap_generic */ class kolab_ldap extends rcube_ldap_generic { private $conf = array(); private $fieldmap = array(); private $rcache; function __construct($p) { $rcmail = rcube::get_instance(); $this->conf = $p; $this->conf['kolab_auth_user_displayname'] = $rcmail->config->get('kolab_auth_user_displayname', '{name}'); $this->fieldmap = $p['fieldmap']; $this->fieldmap['uid'] = 'uid'; $p['attributes'] = array_values($this->fieldmap); $p['debug'] = (bool) $rcmail->config->get('ldap_debug'); if ($cache_type = $rcmail->config->get('ldap_cache', 'db')) { $cache_ttl = $rcmail->config->get('ldap_cache_ttl', '10m'); $this->cache = $rcmail->get_cache('LDAP.kolab_cache', $cache_type, $cache_ttl); } // Connect to the server (with bind) parent::__construct($p); $this->_connect(); $rcmail->add_shutdown_function(array($this, 'close')); } /** * Establish a connection to the LDAP server */ private function _connect() { // try to connect + bind for every host configured // with OpenLDAP 2.x ldap_connect() always succeeds but ldap_bind will fail if host isn't reachable // see http://www.php.net/manual/en/function.ldap-connect.php foreach ((array)$this->config['hosts'] as $host) { // skip host if connection failed if (!$this->connect($host)) { continue; } $bind_pass = $this->config['bind_pass'] ?? null; $bind_user = $this->config['bind_user'] ?? null; $bind_dn = $this->config['bind_dn']; $base_dn = $this->config['base_dn']; $groups_base_dn = $this->config['groups']['base_dn'] ?: $base_dn; // User specific access, generate the proper values to use. if ($this->config['user_specific']) { $rcube = rcube::get_instance(); // No password set, use the session password if (empty($bind_pass)) { $bind_pass = $rcube->get_user_password(); } $u = null; // Get the pieces needed for variable replacement. if ($fu = ($rcube->get_user_email() ?: ($this->config['username'] ?? null))) { list($u, $d) = explode('@', $fu); } else { $d = $this->config['mail_domain'] ?? null; } $dc = 'dc=' . strtr($d, array('.' => ',dc=')); // hierarchal domain string // resolve $dc through LDAP if (!empty($this->config['domain_filter']) && !empty($this->config['search_bind_dn'])) { $this->bind($this->config['search_bind_dn'], $this->config['search_bind_pw']); $dc = $this->domain_root_dn($d); } $replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u); // Search for the dn to use to authenticate - if ($this->config['search_base_dn'] && $this->config['search_filter'] + if (($this->config['search_base_dn'] ?? false) && ($this->config['search_filter'] ?? false) && (strstr($bind_dn, '%dn') || strstr($base_dn, '%dn') || strstr($groups_base_dn, '%dn')) ) { $search_attribs = array('uid'); if ($search_bind_attrib = (array) $this->config['search_bind_attrib']) { foreach ($search_bind_attrib as $r => $attr) { $search_attribs[] = $attr; $replaces[$r] = ''; } } $search_bind_dn = strtr($this->config['search_bind_dn'], $replaces); $search_base_dn = strtr($this->config['search_base_dn'], $replaces); $search_filter = strtr($this->config['search_filter'], $replaces); $cache_key = 'DN.' . md5("$host:$search_bind_dn:$search_base_dn:$search_filter:" . $this->config['search_bind_pw']); if ($this->cache && ($dn = $this->cache->get($cache_key))) { $replaces['%dn'] = $dn; } else { $ldap = $this; if (!empty($search_bind_dn) && !empty($this->config['search_bind_pw'])) { // To protect from "Critical extension is unavailable" error // we need to use a separate LDAP connection if (!empty($this->config['vlv'])) { $ldap = new rcube_ldap_generic($this->config); $ldap->config_set(array('cache' => $this->cache, 'debug' => $this->debug)); if (!$ldap->connect($host)) { continue; } } if (!$ldap->bind($search_bind_dn, $this->config['search_bind_pw'])) { continue; // bind failed, try next host } } $res = $ldap->search($search_base_dn, $search_filter, 'sub', $search_attribs); if ($res) { $res->rewind(); $replaces['%dn'] = key($res->entries(true)); // add more replacements from 'search_bind_attrib' config if ($search_bind_attrib) { $res = $res->current(); foreach ($search_bind_attrib as $r => $attr) { $replaces[$r] = $res[$attr][0]; } } } if ($ldap != $this) { $ldap->close(); } } // DN not found if (empty($replaces['%dn'])) { if (!empty($this->config['search_dn_default'])) $replaces['%dn'] = $this->config['search_dn_default']; else { rcube::raise_error(array( 'code' => 100, 'type' => 'ldap', 'file' => __FILE__, 'line' => __LINE__, 'message' => "DN not found using LDAP search."), true); continue; } } if ($this->cache && !empty($replaces['%dn'])) { $this->cache->set($cache_key, $replaces['%dn']); } } // Replace the bind_dn and base_dn variables. $bind_dn = strtr($bind_dn, $replaces); $base_dn = strtr($base_dn, $replaces); $groups_base_dn = strtr($groups_base_dn, $replaces); // replace placeholders in filter settings if (!empty($this->config['filter'])) { $this->config['filter'] = strtr($this->config['filter'], $replaces); } foreach (array('base_dn', 'filter', 'member_filter') as $k) { if (!empty($this->config['groups'][$k])) { $this->config['groups'][$k] = strtr($this->config['groups'][$k], $replaces); } } if (empty($bind_user)) { $bind_user = $u; } } if (empty($bind_pass)) { $this->ready = true; } else { if (!empty($this->config['auth_cid'])) { $this->ready = $this->sasl_bind($this->config['auth_cid'], $bind_pass, $bind_dn); } else if (!empty($bind_dn)) { $this->ready = $this->bind($bind_dn, $bind_pass); } else { $this->ready = $this->sasl_bind($bind_user, $bind_pass); } } // connection established, we're done here if ($this->ready) { break; } } // end foreach hosts if (!is_resource($this->conn)) { rcube::raise_error(array('code' => 100, 'type' => 'ldap', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Could not connect to any LDAP server, last tried $host"), true); $this->ready = false; } return $this->ready; } /** * Fetches user data from LDAP addressbook */ function get_user_record($user, $host) { $rcmail = rcube::get_instance(); $filter = $rcmail->config->get('kolab_auth_filter'); $filter = $this->parse_vars($filter, $user, $host); $base_dn = $this->parse_vars($this->config['base_dn'], $user, $host); $scope = $this->config['scope']; // @TODO: print error if filter is empty // get record if ($result = parent::search($base_dn, $filter, $scope, $this->attributes)) { if ($result->count() == 1) { $entries = $result->entries(true); $dn = key($entries); $entry = array_pop($entries); $entry = $this->field_mapping($dn, $entry); return $entry; } } } /** * Fetches user data from LDAP addressbook */ function get_user_groups($dn, $user, $host) { if (empty($dn) || empty($this->config['groups'])) { return array(); } $base_dn = $this->parse_vars($this->config['groups']['base_dn'], $user, $host); $name_attr = $this->config['groups']['name_attr'] ? $this->config['groups']['name_attr'] : 'cn'; $member_attr = $this->get_group_member_attr(); $filter = "(member=$dn)(uniqueMember=$dn)"; if ($member_attr != 'member' && $member_attr != 'uniqueMember') $filter .= "($member_attr=$dn)"; $filter = strtr("(|$filter)", array("\\" => "\\\\")); $result = parent::search($base_dn, $filter, 'sub', array('dn', $name_attr)); if (!$result) { return array(); } $groups = array(); foreach ($result as $entry) { $dn = $entry['dn']; $entry = rcube_ldap_generic::normalize_entry($entry); $groups[$dn] = $entry[$name_attr]; } return $groups; } /** * Get a specific LDAP record * * @param string DN * * @return array Record data */ function get_record($dn) { if (!$this->ready) { return; } if ($rec = $this->get_entry($dn, $this->attributes)) { $rec = rcube_ldap_generic::normalize_entry($rec); $rec = $this->field_mapping($dn, $rec); } return $rec; } /** * Replace LDAP record data items * * @param string $dn DN * @param array $entry LDAP entry * * return bool True on success, False on failure */ function replace($dn, $entry) { // fields mapping foreach ($this->fieldmap as $field => $attr) { if (array_key_exists($field, $entry)) { $entry[$attr] = $entry[$field]; if ($attr != $field) { unset($entry[$field]); } } } return $this->mod_replace($dn, $entry); } /** * Search records (simplified version of rcube_ldap::search) * * @param mixed $fields The field name or array of field names to search in * @param string $value Search value * @param int $mode Matching mode: * 0 - partial (*abc*), * 1 - strict (=), * 2 - prefix (abc*) * @param array $required List of fields that cannot be empty * @param int $limit Number of records * @param int $count Returns the number of records found * * @return array List of LDAP records found */ function dosearch($fields, $value, $mode=1, $required = array(), $limit = 0, &$count = 0) { if (empty($fields)) { return array(); } $mode = intval($mode); // try to resolve field names into ldap attributes $fieldmap = $this->fieldmap; $attrs = array_map(function($f) use ($fieldmap) { return array_key_exists($f, $fieldmap) ? $fieldmap[$f] : $f; }, (array)$fields); // compose a full-text-search-like filter if (count($attrs) > 1 || $mode != 1) { $filter = self::fulltext_search_filter($value, $attrs, $mode); } // direct search else { $field = $attrs[0]; $filter = "($field=" . self::quote_string($value) . ")"; } // add required (non empty) fields filter $req_filter = ''; foreach ((array)$required as $field) { $attr = array_key_exists($field, $this->fieldmap) ? $this->fieldmap[$field] : $field; // only add if required field is not already in search filter if (!in_array($attr, $attrs)) { $req_filter .= "($attr=*)"; } } if (!empty($req_filter)) { $filter = '(&' . $req_filter . $filter . ')'; } // avoid double-wildcard if $value is empty $filter = preg_replace('/\*+/', '*', $filter); // add general filter to query if (!empty($this->config['filter'])) { $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->config['filter']) . ')' . $filter . ')'; } $base_dn = $this->parse_vars($this->config['base_dn']); $scope = $this->config['scope']; $attrs = array_values($this->fieldmap); $list = array(); if ($result = $this->search($base_dn, $filter, $scope, $attrs)) { $count = $result->count(); $i = 0; foreach ($result as $entry) { if ($limit && $limit <= $i) { break; } $dn = $entry['dn']; $entry = rcube_ldap_generic::normalize_entry($entry); $list[$dn] = $this->field_mapping($dn, $entry); $i++; } } return $list; } /** * Set filter used in search() */ function set_filter($filter) { $this->config['filter'] = $filter; } /** * Maps LDAP attributes to defined fields */ protected function field_mapping($dn, $entry) { $entry['dn'] = $dn; // fields mapping foreach ($this->fieldmap as $field => $attr) { // $entry might be indexed by lower-case attribute names $attr_lc = strtolower($attr); if (isset($entry[$attr_lc])) { $entry[$field] = $entry[$attr_lc]; } else if (isset($entry[$attr])) { $entry[$field] = $entry[$attr]; } } // compose display name according to config if (empty($this->fieldmap['displayname'])) { $entry['displayname'] = rcube_addressbook::compose_search_name( $entry, $entry['email'], $entry['name'], $this->conf['kolab_auth_user_displayname'] ); } return $entry; } /** * Detects group member attribute name */ private function get_group_member_attr($object_classes = array()) { if (empty($object_classes)) { $object_classes = $this->config['groups']['object_classes']; } if (!empty($object_classes)) { foreach ((array)$object_classes as $oc) { switch (strtolower($oc)) { case 'group': case 'groupofnames': case 'kolabgroupofnames': $member_attr = 'member'; break; case 'groupofuniquenames': case 'kolabgroupofuniquenames': $member_attr = 'uniqueMember'; break; } } } if (!empty($member_attr)) { return $member_attr; } if (!empty($this->config['groups']['member_attr'])) { return $this->config['groups']['member_attr']; } return 'member'; } /** * Prepares filter query for LDAP search */ function parse_vars($str, $user = null, $host = null) { // When authenticating user $user is always set // if not set it means we use this LDAP object for other // purposes, e.g. kolab_delegation, then username with // correct domain is in a session if (!$user) { $user = $_SESSION['username']; } $dc = null; if (isset($this->icache[$user])) { list($user, $dc) = $this->icache[$user]; } else { $orig_user = $user; $rcmail = rcube::get_instance(); // get default domain if ($username_domain = $rcmail->config->get('username_domain')) { if ($host && is_array($username_domain) && isset($username_domain[$host])) { $domain = rcube_utils::parse_host($username_domain[$host], $host); } else if (is_string($username_domain)) { $domain = rcube_utils::parse_host($username_domain, $host); } } // realmed username (with domain) if (strpos($user, '@')) { list($usr, $dom) = explode('@', $user); // unrealm domain, user login can contain a domain alias if ($dom != $domain && ($dc = $this->domain_root_dn($dom))) { // @FIXME: we should replace domain in $user, I suppose } } else if ($domain) { $user .= '@' . $domain; } $this->icache[$orig_user] = array($user, $dc); } // replace variables in filter list($u, $d) = explode('@', $user); // hierarchal domain string if (empty($dc)) { $dc = 'dc=' . strtr($d, array('.' => ',dc=')); } $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u); $this->parse_replaces = $replaces; return strtr($str, $replaces); } /** * Returns variables used for replacement in (last) parse_vars() call * * @return array Variable-value hash array */ public function get_parse_vars() { return $this->parse_replaces; } /** * Register additional fields */ public function extend_fieldmap($map) { foreach ((array)$map as $name => $attr) { if (!in_array($attr, $this->attributes)) { $this->attributes[] = $attr; $this->fieldmap[$name] = $attr; } } } /** * HTML-safe DN string encoding * * @param string $str DN string * * @return string Encoded HTML identifier string */ static function dn_encode($str) { return rtrim(strtr(base64_encode($str), '+/', '-_'), '='); } /** * Decodes DN string encoded with _dn_encode() * * @param string $str Encoded HTML identifier string * * @return string DN string */ static function dn_decode($str) { $str = str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT); return base64_decode($str); } }