From e69e9b90ae6d187d4038897f007d2f786c1132a3 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Tue, 25 Jun 2013 12:27:26 +0200 Subject: [PATCH] Make kolab_auth's LDAP class be based on new rcube_ldap_generic class. Move kolab_auth_ldap into separate file. Some improvements, including performance improvement in kolab_delegate --- plugins/kolab_auth/kolab_auth.php | 119 +----- plugins/kolab_auth/kolab_auth_ldap.php | 403 ++++++++++++++++++ plugins/kolab_auth/package.xml | 10 +- .../kolab_delegation_engine.php | 114 +++-- 4 files changed, 486 insertions(+), 160 deletions(-) create mode 100644 plugins/kolab_auth/kolab_auth_ldap.php diff --git a/plugins/kolab_auth/kolab_auth.php b/plugins/kolab_auth/kolab_auth.php index f5119497..5579743f 100644 --- a/plugins/kolab_auth/kolab_auth.php +++ b/plugins/kolab_auth/kolab_auth.php @@ -61,12 +61,12 @@ class kolab_auth extends rcube_plugin $rcmail->config->set('imap_debug', true); $rcmail->config->set('ldap_debug', true); $rcmail->config->set('smtp_debug', true); - } } - public function startup($args) { + public function startup($args) + { // Arguments are task / action, not interested if (!empty($_SESSION['user_roledns'])) { $this->load_user_role_plugins_and_settings($_SESSION['user_roledns']); @@ -75,7 +75,8 @@ class kolab_auth extends rcube_plugin return $args; } - public function load_user_role_plugins_and_settings($role_dns) { + public function load_user_role_plugins_and_settings($role_dns) + { $rcmail = rcube::get_instance(); $this->load_config(); @@ -151,7 +152,8 @@ class kolab_auth extends rcube_plugin } } - public function write_log($args) { + public function write_log($args) + { $rcmail = rcube::get_instance(); if (!$rcmail->config->get('kolab_auth_auditlog', false)) { @@ -286,7 +288,7 @@ class kolab_auth extends rcube_plugin } // Find user record in LDAP - $record = $this->get_user_record($user, $host); + $record = $ldap->get_user_record($user, $host); if (empty($record)) { $args['abort'] = true; @@ -309,8 +311,7 @@ class kolab_auth extends rcube_plugin // Login As... if (!empty($loginas) && $admin_login) { // Authenticate to LDAP - $dn = rcube_ldap::dn_decode($record['ID']); - $result = $ldap->bind($dn, $pass); + $result = $ldap->bind($record['dn'], $pass); if (!$result) { $args['abort'] = true; @@ -318,32 +319,24 @@ class kolab_auth extends rcube_plugin } // check if the original user has/belongs to administrative role/group - $isadmin = false; - $group = $rcmail->config->get('kolab_auth_group'); - $role_attr = $rcmail->config->get('kolab_auth_role'); - $role_dn = $rcmail->config->get('kolab_auth_role_value'); + $isadmin = false; + $group = $rcmail->config->get('kolab_auth_group'); + $role_dn = $rcmail->config->get('kolab_auth_role_value'); // check role attribute if (!empty($role_attr) && !empty($role_dn) && !empty($record[$role_attr])) { - $role_dn = $this->parse_vars($role_dn, $user, $host); - foreach ((array)$record[$role_attr] as $role) { - if ($role == $role_dn) { - $isadmin = true; - break; - } + $role_dn = $ldap->parse_vars($role_dn, $user, $host); + if (in_array($role_dn, (array)$record[$role_attr])) { + $isadmin = true; } } // check group if (!$isadmin && !empty($group)) { - $groups = $ldap->get_record_groups($record['ID']); - foreach (array_keys($groups) as $g) { - if ($group == rcube_ldap::dn_decode($g)) { - $isadmin = true; - break; - } + $groups = $ldap->get_user_groups($record['dn'], $user, $host); + if (in_array($group, $groups)) { + $isadmin = true; } - } // Save original user login for log (see below) @@ -358,7 +351,7 @@ class kolab_auth extends rcube_plugin // user has the privilage, get "login as" user credentials if ($isadmin) { - $record = $this->get_user_record($loginas, $host); + $record = $ldap->get_user_record($loginas, $host); } if (empty($record)) { @@ -376,7 +369,7 @@ class kolab_auth extends rcube_plugin // Store UID and DN of logged user in session for use by other plugins $_SESSION['kolab_uid'] = is_array($record['uid']) ? $record['uid'][0] : $record['uid']; - $_SESSION['kolab_dn'] = $record['ID']; // encoded + $_SESSION['kolab_dn'] = $record['dn']; // Set user login if ($login_attr) { @@ -485,80 +478,10 @@ class kolab_auth extends rcube_plugin return null; } - self::$ldap = new kolab_auth_ldap_backend( - $addressbook, - $rcmail->config->get('ldap_debug'), - $rcmail->config->mail_domain($_SESSION['imap_host']) - ); + require_once __DIR__ . '/kolab_auth_ldap.php'; - $rcmail->add_shutdown_function(array(self::$ldap, 'close')); + self::$ldap = new kolab_auth_ldap($addressbook); return self::$ldap; } - - /** - * Fetches user data from LDAP addressbook - */ - private function get_user_record($user, $host) - { - $rcmail = rcube::get_instance(); - $filter = $rcmail->config->get('kolab_auth_filter'); - $filter = $this->parse_vars($filter, $user, $host); - $ldap = self::ldap(); - - // reset old result - $ldap->reset(); - - // get record - $ldap->set_filter($filter); - $results = $ldap->list_records(); - - if (count($results->records) == 1) { - return $results->records[0]; - } - } - - /** - * Prepares filter query for LDAP search - */ - private function parse_vars($str, $user, $host) - { - $rcmail = rcube::get_instance(); - $domain = $rcmail->config->get('username_domain'); - - if (!empty($domain) && strpos($user, '@') === false) { - if (is_array($domain) && isset($domain[$host])) { - $user .= '@'.rcube_utils::parse_host($domain[$host], $host); - } - else if (is_string($domain)) { - $user .= '@'.rcube_utils::parse_host($domain, $host); - } - } - - // replace variables in filter - list($u, $d) = explode('@', $user); - $dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string - $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u); - - return strtr($str, $replaces); - } -} - -/** - * Wrapper class for rcube_ldap addressbook - */ -class kolab_auth_ldap_backend extends rcube_ldap -{ - function __construct($p, $debug=false, $mail_domain=null) - { - parent::__construct($p, $debug, $mail_domain); - $this->fieldmap['uid'] = 'uid'; - } - - function set_filter($filter) - { - if ($filter) { - $this->prop['filter'] = $filter; - } - } } diff --git a/plugins/kolab_auth/kolab_auth_ldap.php b/plugins/kolab_auth/kolab_auth_ldap.php new file mode 100644 index 00000000..b9e557ec --- /dev/null +++ b/plugins/kolab_auth/kolab_auth_ldap.php @@ -0,0 +1,403 @@ + + * + * Copyright (C) 2011-2013, 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 + * 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 . + */ + +/** + * Wrapper class for rcube_ldap_generic + */ +class kolab_auth_ldap extends rcube_ldap_generic +{ + + function __construct($p) + { + $rcmail = rcube::get_instance(); + + $this->debug = (bool) $rcmail->config->get('ldap_debug'); + $this->domain = $rcmail->config->get('username_domain'); + $this->fieldmap = $p['fieldmap']; + $this->fieldmap['uid'] = 'uid'; + + $p['attributes'] = array_values($this->fieldmap); + + // 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() + { + $rcube = rcube::get_instance(); + + // 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']; + $bind_user = $this->config['bind_user']; + $bind_dn = $this->config['bind_dn']; + + if (empty($bind_pass)) { + $this->ready = true; + } + else { + if (!empty($bind_dn)) { + $this->ready = $this->bind($bind_dn, $bind_pass); + } + else if (!empty($this->config['auth_cid'])) { + $this->ready = $this->sasl_bind($this->config['auth_cid'], $bind_pass, $bind_user); + } + 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']; + + // 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) { + $entry = rcube_ldap_generic::normalize_entry($entry); + if (!$entry['dn']) { + $entry['dn'] = $result->get_dn(); + } + $groups[$entry['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)) { + $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]; + unset($entry[$field]); + } + } + + return $this->mod_replace($dn, $entry); + } + + /** + * Search records (simplified version of rcube_ldap::search) + * + * @param mixed $fields The field name of array of field names to search in + * @param mixed $value Search value (or array of values when $fields is array) + * @param int $mode Matching mode: + * 0 - partial (*abc*), + * 1 - strict (=), + * 2 - prefix (abc*) + * @param boolean $select True if results are requested, False if count only + * @param array $required List of fields that cannot be empty + * @param int $limit Number of records + * + * @return array List or false on error + */ + function search($fields, $value, $mode=1, $required = array(), $limit = 0) + { + $mode = intval($mode); + + // use AND operator for advanced searches + $filter = is_array($value) ? '(&' : '(|'; + + // set wildcards + $wp = $ws = ''; + if (!empty($this->config['fuzzy_search']) && $mode != 1) { + $ws = '*'; + if (!$mode) { + $wp = '*'; + } + } + + foreach ((array)$fields as $idx => $field) { + $val = is_array($value) ? $value[$idx] : $value; + if ($attrs = (array) $this->fieldmap[$field]) { + if (count($attrs) > 1) + $filter .= '(|'; + foreach ($attrs as $f) + $filter .= "($f=$wp" . rcube_ldap_generic::quote_string($val) . "$ws)"; + if (count($attrs) > 1) + $filter .= ')'; + } + } + $filter .= ')'; + + // add required (non empty) fields filter + $req_filter = ''; + + foreach ((array)$required as $field) { + if (in_array($field, (array)$fields)) // required field is already in search filter + continue; + if ($attrs = (array) $this->fieldmap[$field]) { + if (count($attrs) > 1) + $req_filter .= '(|'; + foreach ($attrs as $f) + $req_filter .= "($f=*)"; + if (count($attrs) > 1) + $req_filter .= ')'; + } + } + + 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'], $_SESSION['username']); + $scope = $this->config['scope']; + $attrs = array_values($this->fieldmap); + $list = array(); + + if ($result = parent::search($base_dn, $filter, $scope, $attrs)) { + $i = 0; + foreach ($result as $entry) { + if ($limit && $limit <= $i) { + break; + } + $dn = $result->get_dn(); + $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) { + if (isset($entry[$attr])) { + $entry[$field] = $entry[$attr]; + } + } + + 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, $host = null) + { + if (!empty($this->domain) && strpos($user, '@') === false) { + if ($host && is_array($this->domain) && isset($this->domain[$host])) { + $user .= '@'.rcube_utils::parse_host($this->domain[$host], $host); + } + else if (is_string($this->domain)) { + $user .= '@'.rcube_utils::parse_host($this->domain, $host); + } + } + + // replace variables in filter + list($u, $d) = explode('@', $user); + $dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string + $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u); + + return strtr($str, $replaces); + } + + /** + * 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); + } +} diff --git a/plugins/kolab_auth/package.xml b/plugins/kolab_auth/package.xml index 2d75d83f..00bc969b 100644 --- a/plugins/kolab_auth/package.xml +++ b/plugins/kolab_auth/package.xml @@ -18,10 +18,10 @@ machniak@kolabsys.com yes - 2012-12-19 + 2013-06-25 - 0.6 - 0.1 + 0.7 + 0.2 stable @@ -35,6 +35,10 @@ + + + + diff --git a/plugins/kolab_delegation/kolab_delegation_engine.php b/plugins/kolab_delegation/kolab_delegation_engine.php index 26f6b38a..53813bb7 100644 --- a/plugins/kolab_delegation/kolab_delegation_engine.php +++ b/plugins/kolab_delegation/kolab_delegation_engine.php @@ -61,25 +61,24 @@ class kolab_delegation_engine $delegate = $this->delegate_get($delegate); } - $dn = $delegate['ID']; - $list = $this->list_delegates(); - $user = $this->user(); - + $dn = $delegate['ID']; if (empty($delegate) || empty($dn)) { return false; } + $list = $this->list_delegates(); + $user = $this->user(); + // add delegate to the list $list = array_keys((array)$list); $list = array_filter($list); if (!in_array($dn, $list)) { $list[] = $dn; } - $list = array_map(array('rcube_ldap', 'dn_decode'), $list); - $user[$this->ldap_delegate_field] = $list; + $list = array_map(array('kolab_auth_ldap', 'dn_decode'), $list); // update user record - $result = $this->user_update($user); + $result = $this->user_update_delegates($list); // Set ACL on folders if ($result && !empty($acl)) { @@ -141,11 +140,11 @@ class kolab_delegation_engine // remove delegate from the list unset($list[$dn]); $list = array_keys($list); - $list = array_map(array('rcube_ldap', 'dn_decode'), $list); + $list = array_map(array('kolab_auth_ldap', 'dn_decode'), $list); $user[$this->ldap_delegate_field] = $list; // update user record - $result = $this->user_update($user); + $result = $this->user_update_delegates($list); // remove ACL if ($result && $acl_del) { @@ -164,25 +163,28 @@ class kolab_delegation_engine */ public function delegate_get($dn) { - $ldap = $this->ldap(); + // use internal cache so we not query LDAP more than once per request + if (!isset($this->cache[$dn])) { + $ldap = $this->ldap(); - if (!$ldap) { - return array(); + if (!$ldap || empty($dn)) { + return array(); + } + + // Get delegate + $user = $ldap->get_record(kolab_auth_ldap::dn_decode($dn)); + + if (empty($user)) { + return array(); + } + + $delegate = $this->parse_ldap_record($user); + $delegate['ID'] = $dn; + + $this->cache[$dn] = $delegate; } - $ldap->reset(); - - // Get delegate - $user = $ldap->get_record($dn, true); - - if (empty($user)) { - return array(); - } - - $delegate = $this->parse_ldap_record($user); - $delegate['ID'] = $dn; - - return $delegate; + return $this->cache[$dn]; } /** @@ -200,13 +202,13 @@ class kolab_delegation_engine return array(); } - $ldap->reset(); - $list = $ldap->search($this->ldap_login_field, $login, 1); - if ($list->count == 1) { - $user = $list->next(); - return $this->parse_ldap_record($user); + if (count($list) == 1) { + $dn = key($list); + $user = $list[$dn]; + + return $this->parse_ldap_record($user, $dn); } } @@ -248,9 +250,8 @@ class kolab_delegation_engine public function list_delegates() { $result = array(); - - $ldap = $this->ldap(); - $user = $this->user(); + $ldap = $this->ldap(); + $user = $this->user(); if (empty($ldap) || empty($user)) { return array(); @@ -261,12 +262,11 @@ class kolab_delegation_engine if (!empty($delegates)) { foreach ((array)$delegates as $dn) { - $ldap->reset(); - $delegate = $ldap->get_record(rcube_ldap::dn_encode($dn), true); - $data = $this->parse_ldap_record($delegate); + $delegate = $ldap->get_record($dn); + $data = $this->parse_ldap_record($delegate, $dn); if (!empty($data) && !empty($data['name'])) { - $result[$delegate['ID']] = $data['name']; + $result[$data['ID']] = $data['name']; } } } @@ -282,18 +282,17 @@ class kolab_delegation_engine public function list_delegators() { $result = array(); - - $ldap = $this->ldap(); + $ldap = $this->ldap(); if (empty($ldap) || empty($this->ldap_dn)) { return array(); } - $ldap->reset(); - $list = $ldap->search($this->ldap_delegate_field, rcube_ldap::dn_decode($this->ldap_dn), 1); + $list = $ldap->search($this->ldap_delegate_field, $this->ldap_dn, 1); - while ($delegator = $list->iterate()) { - $result[$delegator['ID']] = $this->parse_ldap_record($delegator); + foreach ($list as $dn => $delegator) { + $delegator = $this->parse_ldap_record($delegator, $dn); + $result[$delegator['ID']] = $delegator; } return $result; @@ -427,11 +426,9 @@ class kolab_delegation_engine $fields = array_unique(array_filter(array_merge((array)$this->ldap_name_field, (array)$this->ldap_login_field))); $users = array(); - $ldap->reset(); - $ldap->set_pagesize($max); - $result = $ldap->search($fields, $search, $mode, true, false, (array)$this->ldap_login_field); + $result = $ldap->search($fields, $search, $mode, (array)$this->ldap_login_field, $max); - foreach ($result->records as $record) { + foreach ($result as $record) { $user = $this->parse_ldap_record($record); if ($user['name']) { @@ -447,7 +444,7 @@ class kolab_delegation_engine /** * Extract delegate identifiers and pretty name from LDAP record */ - private function parse_ldap_record($data) + private function parse_ldap_record($data, $dn = null) { $email = array(); $uid = $data[$this->ldap_login_field]; @@ -496,12 +493,12 @@ class kolab_delegation_engine } return array( + 'ID' => kolab_auth_ldap::dn_encode($dn), 'uid' => $uid, 'name' => $name, 'realname' => $realname, 'imap_uid' => $imap_uid, 'email' => $email, - 'ID' => $data['ID'], 'organization' => $organization, ); } @@ -520,8 +517,6 @@ class kolab_delegation_engine return array(); } - $ldap->reset(); - // Get current user record $this->cache['user'] = $ldap->get_record($this->ldap_dn, true); } @@ -547,27 +542,28 @@ class kolab_delegation_engine /** * Update LDAP record of current user * - * @param array User data + * @param array List of delegates */ - public function user_update($user) + public function user_update_delegates($list) { $ldap = $this->ldap(); + $pass = $this->rc->decrypt($_SESSION['password']); if (!$ldap) { return false; } - $dn = rcube_ldap::dn_decode($this->ldap_dn); - $pass = $this->rc->decrypt($_SESSION['password']); - // need to bind as self for sufficient privilages - if (!$ldap->bind($dn, $pass)) { + if (!$ldap->bind($this->ldap_dn, $pass)) { return false; } + $user[$this->ldap_delegate_field] = $list; + unset($this->cache['user']); - // update user record - return $ldap->update($this->ldap_dn, $user); + + // replace delegators list in user record + return $ldap->replace($this->ldap_dn, $user); } /**