From 80a5241a9d9ac08f2971152ff032218e00397ed7 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Mon, 19 Aug 2019 14:06:11 +0000 Subject: [PATCH] Support user_specific source in kolab_users_directory (Bifrost#T236416) Move kolab_auth/kolab_auth_ldap to libkolab/lib/kolab_ldap. It ends up much simpler to add user_specific support and unify some code than replace use of kolab_auth_ldap with rcube_ldap. This means that libkolab plugin does not depend on kolab_auth plugin anymore, but kolab_auth depends on libkolab, which is better situation. --- plugins/kolab_auth/composer.json | 3 +- plugins/kolab_auth/kolab_auth.php | 25 +-- plugins/kolab_delegation/composer.json | 4 +- .../kolab_delegation_engine.php | 36 +--- .../lib/kolab_ldap.php} | 155 ++++++++++++++++-- plugins/libkolab/lib/kolab_storage.php | 52 +++--- 6 files changed, 193 insertions(+), 82 deletions(-) rename plugins/{kolab_auth/kolab_auth_ldap.php => libkolab/lib/kolab_ldap.php} (67%) diff --git a/plugins/kolab_auth/composer.json b/plugins/kolab_auth/composer.json index 79594422..b0c4256b 100644 --- a/plugins/kolab_auth/composer.json +++ b/plugins/kolab_auth/composer.json @@ -25,6 +25,7 @@ ], "require": { "php": ">=5.3.0", - "roundcube/plugin-installer": ">=0.1.3" + "roundcube/plugin-installer": ">=0.1.3", + "kolab/libkolab": ">=3.5.1" } } diff --git a/plugins/kolab_auth/kolab_auth.php b/plugins/kolab_auth/kolab_auth.php index af4f5f7b..338fd6d1 100644 --- a/plugins/kolab_auth/kolab_auth.php +++ b/plugins/kolab_auth/kolab_auth.php @@ -39,6 +39,7 @@ class kolab_auth extends rcube_plugin $rcmail = rcube::get_instance(); $this->load_config(); + $this->require_plugin('libkolab'); $this->add_hook('authenticate', array($this, 'authenticate')); $this->add_hook('startup', array($this, 'startup')); @@ -796,28 +797,12 @@ class kolab_auth extends rcube_plugin */ public static function ldap() { + self::$ldap = kolab_storage::ldap('kolab_auth_addressbook'); + if (self::$ldap) { - return self::$ldap; + self::$ldap->extend_fieldmap(array('uniqueid' => 'nsuniqueid')); } - $rcmail = rcube::get_instance(); - $addressbook = $rcmail->config->get('kolab_auth_addressbook'); - - if (!is_array($addressbook)) { - $ldap_config = (array)$rcmail->config->get('ldap_public'); - $addressbook = $ldap_config[$addressbook]; - } - - if (empty($addressbook)) { - return null; - } - - $addressbook['fieldmap']['uniqueid'] = 'nsuniqueid'; - - require_once __DIR__ . '/kolab_auth_ldap.php'; - - self::$ldap = new kolab_auth_ldap($addressbook); - return self::$ldap; } @@ -834,7 +819,7 @@ class kolab_auth extends rcube_plugin /** * Parses LDAP DN string with replacing supported variables. - * See kolab_auth_ldap::parse_vars() + * See kolab_ldap::parse_vars() * * @param string $str LDAP DN string * diff --git a/plugins/kolab_delegation/composer.json b/plugins/kolab_delegation/composer.json index 7a31ec5e..4617c8c7 100644 --- a/plugins/kolab_delegation/composer.json +++ b/plugins/kolab_delegation/composer.json @@ -21,7 +21,7 @@ "require": { "php": ">=5.3.0", "roundcube/plugin-installer": ">=0.1.3", - "kolab/libkolab": ">=3.4.0", - "kolab/kolab_auth": ">=3.4.0" + "kolab/libkolab": ">=3.5.1", + "kolab/kolab_auth": ">=3.5.1" } } diff --git a/plugins/kolab_delegation/kolab_delegation_engine.php b/plugins/kolab_delegation/kolab_delegation_engine.php index d548f242..a7a5956e 100644 --- a/plugins/kolab_delegation/kolab_delegation_engine.php +++ b/plugins/kolab_delegation/kolab_delegation_engine.php @@ -79,7 +79,7 @@ class kolab_delegation_engine // add delegate to the list $list[] = $dn; - $list = array_map(array('kolab_auth_ldap', 'dn_decode'), $list); + $list = array_map(array('kolab_ldap', 'dn_decode'), $list); // update user record $result = $this->user_update_delegates($list); @@ -149,7 +149,7 @@ class kolab_delegation_engine // remove delegate from the list unset($list[$dn]); $list = array_keys($list); - $list = array_map(array('kolab_auth_ldap', 'dn_decode'), $list); + $list = array_map(array('kolab_ldap', 'dn_decode'), $list); $user[$this->ldap_delegate_field] = $list; // update user record @@ -181,7 +181,7 @@ class kolab_delegation_engine } // Get delegate - $user = $ldap->get_record(kolab_auth_ldap::dn_decode($dn)); + $user = $ldap->get_record(kolab_ldap::dn_decode($dn)); if (empty($user)) { return array(); @@ -230,27 +230,9 @@ class kolab_delegation_engine return $this->ldap; } - if ($addressbook = $this->rc->config->get('kolab_delegation_addressbook')) { - if (!is_array($addressbook)) { - $ldap_config = (array) $this->rc->config->get('ldap_public'); - $addressbook = $ldap_config[$addressbook]; - } + $this->ldap = kolab_storage::ldap('kolab_delegation_addressbook'); - if (!empty($addressbook)) { - require_once __DIR__ . '/../kolab_auth/kolab_auth_ldap.php'; - - $ldap = new kolab_auth_ldap($addressbook); - } - } - - // Fallback to kolab_auth plugin's addressbook - if (!$ldap) { - $ldap = kolab_auth::ldap(); - } - - $this->ldap = $ldap; - - if (!$ldap || !$ldap->ready) { + if (!$this->ldap || !$this->ldap->ready) { return null; } @@ -270,10 +252,10 @@ class kolab_delegation_engine // 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')); - $ldap->set_filter($this->ldap_filter); - $ldap->extend_fieldmap(array($this->ldap_delegate_field => $this->ldap_delegate_field)); + $this->ldap->set_filter($this->ldap_filter); + $this->ldap->extend_fieldmap(array($this->ldap_delegate_field => $this->ldap_delegate_field)); - return $ldap; + return $this->ldap; } /** @@ -540,7 +522,7 @@ class kolab_delegation_engine } return array( - 'ID' => kolab_auth_ldap::dn_encode($dn), + 'ID' => kolab_ldap::dn_encode($dn), 'uid' => $uid, 'name' => $name, 'realname' => $realname, diff --git a/plugins/kolab_auth/kolab_auth_ldap.php b/plugins/libkolab/lib/kolab_ldap.php similarity index 67% rename from plugins/kolab_auth/kolab_auth_ldap.php rename to plugins/libkolab/lib/kolab_ldap.php index 01aaf582..29984b09 100644 --- a/plugins/kolab_auth/kolab_auth_ldap.php +++ b/plugins/libkolab/lib/kolab_ldap.php @@ -1,12 +1,11 @@ * - * Copyright (C) 2011-2013, Kolab Systems AG + * Copyright (C) 2011-2019, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -25,10 +24,11 @@ /** * Wrapper class for rcube_ldap_generic */ -class kolab_auth_ldap extends rcube_ldap_generic +class kolab_ldap extends rcube_ldap_generic { private $conf = array(); private $fieldmap = array(); + private $rcache; function __construct($p) @@ -44,6 +44,11 @@ class kolab_auth_ldap extends rcube_ldap_generic $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(); @@ -65,19 +70,145 @@ class kolab_auth_ldap extends rcube_ldap_generic continue; } - $bind_pass = $this->config['bind_pass']; - $bind_user = $this->config['bind_user']; - $bind_dn = $this->config['bind_dn']; + $bind_pass = $this->config['bind_pass']; + $bind_user = $this->config['bind_user']; + $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(); + } + + // Get the pieces needed for variable replacement. + if ($fu = ($rcube->get_user_email() ?: $this->config['username'])) { + list($u, $d) = explode('@', $fu); + } + else { + $d = $this->config['mail_domain']; + } + + $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'] + && (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($bind_dn)) { - $this->ready = $this->bind($bind_dn, $bind_pass); + if (!empty($this->config['auth_cid'])) { + $this->ready = $this->sasl_bind($this->config['auth_cid'], $bind_pass, $bind_dn); } - else if (!empty($this->config['auth_cid'])) { - $this->ready = $this->sasl_bind($this->config['auth_cid'], $bind_pass, $bind_user); + else if (!empty($bind_dn)) { + $this->ready = $this->bind($bind_dn, $bind_pass); } else { $this->ready = $this->sasl_bind($bind_user, $bind_pass); @@ -220,7 +351,7 @@ class kolab_auth_ldap extends rcube_ldap_generic * @param int $limit Number of records * @param int $count Returns the number of records found * - * @return array List or false on error + * @return array List of LDAP records found */ function dosearch($fields, $value, $mode=1, $required = array(), $limit = 0, &$count = 0) { diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php index 17041cbf..f75ca7d0 100644 --- a/plugins/libkolab/lib/kolab_storage.php +++ b/plugins/libkolab/lib/kolab_storage.php @@ -48,10 +48,10 @@ class kolab_storage private static $subscriptions; private static $ldapcache = array(); private static $typedata = array(); + private static $ldap = array(); private static $states; private static $config; private static $imap; - private static $ldap; // Default folder names @@ -96,7 +96,7 @@ class kolab_storage 'message' => "required kolabformat module not found" ), true); } - else { + else if (self::$imap->get_error_code()) { rcube::raise_error(array( 'code' => 900, 'type' => 'php', 'message' => "IMAP error" ), true); @@ -116,16 +116,23 @@ class kolab_storage /** * Initializes LDAP object to resolve Kolab users + * + * @param string $name Name of the configuration option with LDAP config */ - public static function ldap() + public static function ldap($name = 'kolab_users_directory') { - if (self::$ldap) { - return self::$ldap; - } - self::setup(); - $config = self::$config->get('kolab_users_directory', self::$config->get('kolab_auth_addressbook')); + $config = self::$config->get($name); + + if (empty($config)) { + $name = 'kolab_auth_addressbook'; + $config = self::$config->get($name); + } + + if (self::$ldap[$name]) { + return self::$ldap[$name]; + } if (!is_array($config)) { $ldap_config = (array)self::$config->get('ldap_public'); @@ -136,17 +143,21 @@ class kolab_storage return null; } + $ldap = new kolab_ldap($config); + // overwrite filter option if ($filter = self::$config->get('kolab_users_filter')) { self::$config->set('kolab_auth_filter', $filter); } - // re-use the LDAP wrapper class from kolab_auth plugin - require_once rtrim(RCUBE_PLUGINS_DIR, '/') . '/kolab_auth/kolab_auth_ldap.php'; + $user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail')); - self::$ldap = new kolab_auth_ldap($config); + //$ldap->set_filter($this->ldap_filter); + $ldap->extend_fieldmap(array($user_attrib => $user_attrib)); - return self::$ldap; + self::$ldap[$name] = $ldap; + + return $ldap; } /** @@ -1485,6 +1496,7 @@ class kolab_storage } /** + * Search users in Kolab LDAP storage * * @param mixed $query Search value (or array of field => value pairs) * @param int $mode Matching mode: 0 - partial (*abc*), 1 - strict (=), 2 - prefix (abc*) @@ -1492,19 +1504,23 @@ class kolab_storage * @param int $limit Maximum number of records * @param int $count Returns the number of records found * - * @return array List or false on error + * @return array List of users */ public static function search_users($query, $mode = 1, $required = array(), $limit = 0, &$count = 0) { $query = str_replace('*', '', $query); // requires a working LDAP setup - if (!self::ldap() || strlen($query) == 0) { + if (!strlen($query) || !($ldap = self::ldap())) { return array(); } + $root = self::namespace_root('other'); + $user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail')); + $search_attrib = self::$config->get('kolab_users_search_attrib', array('cn','mail','alias')); + // search users using the configured attributes - $results = self::$ldap->dosearch(self::$config->get('kolab_users_search_attrib', array('cn','mail','alias')), $query, $mode, $required, $limit, $count); + $results = $ldap->dosearch($search_attrib, $query, $mode, $required, $limit, $count); // exclude myself if ($_SESSION['kolab_dn']) { @@ -1512,9 +1528,6 @@ class kolab_storage } // resolve to IMAP folder name - $root = self::namespace_root('other'); - $user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail')); - array_walk($results, function(&$user, $dn) use ($root, $user_attrib) { list($localpart, ) = explode('@', $user[$user_attrib]); $user['kolabtargetfolder'] = $root . $localpart; @@ -1652,7 +1665,6 @@ class kolab_storage /** * Get user attributes for specified other user (imap) folder identifier. - * Note: This uses LDAP config/code from kolab_auth. * * @param string $folder_id Folder name w/o path (imap user identifier) * @param bool $as_string Return configured display name attribute value @@ -1692,7 +1704,7 @@ class kolab_storage $user = $cache->get($token); } - if (empty($user) && ($ldap = kolab_storage::ldap())) { + if (empty($user) && ($ldap = self::ldap())) { $user = $ldap->get_user_record($token, $_SESSION['imap_host']); if (!empty($user)) {