diff --git a/config/config.ini.sample b/config/config.ini.sample index 90509a3..8fcdf20 100644 --- a/config/config.ini.sample +++ b/config/config.ini.sample @@ -1,89 +1,93 @@ ;; Kolab Free/Busy Service configuration ; Logging configuration [log] driver = file ; supported drivers: file, syslog path = ./logs name = freebusy level = 300 ; (100 = Debug, 200 = Info, 300 = Warn, 400 = Error, 500 = Critical) ;; ;; try local filesystem first (F/B has been generated externally) ;; [directory "local"] type = static filter = "@example.org" fbsource = file:/var/lib/kolab-freebusy/%s.ifb ;; ;; check if primary email address hits a cache file (saves LDAP lookups) ;; [directory "local-cache"] type = static fbsource = file:/var/cache/kolab-freebusy/%s.ifb expires = 10m ;; ;; local Kolab directory server ;; [directory "kolab-people"] type = ldap host = "ldap://localhost:389" bind_dn = "uid=kolab-service,ou=Special Users,dc=example,dc=org" bind_pw = "SomePassword" +; base_dn and filter can use these variables: +; %dc = domain root dn, %u = username part, %s = the full username base_dn = "ou=People,dc=example,dc=org" filter = "(&(objectClass=kolabInetOrgPerson)(|(mail=%s)(alias=%s))" attributes = mail lc_attributes = mail primary_domain = "example.org" - -; %s is replaced by the user's result_attribute found -fbsource = imaps://%s:CyrusAdminPassword@imap.example.org/?proxy_auth=cyrus-admin +; set domain_* options to enforce the resolving of the domain root dn (%dc) through LDAP +domain_base_dn = "cn=kolab,cn=config" +domain_filter = "(&(objectclass=domainrelatedobject)(associateddomain=%s))" +; %mail is replaced by the user's mail attribute found in LDAP +fbsource = imaps://%mail:CyrusAdminPassword@imap.example.org/?proxy_auth=cyrus-admin loglevel = 300 cacheto = /var/cache/kolab-freebusy/%mail.ifb expires = 10m ;; ;; resolve Kolab resources from LDAP and fetch calendar from IMAP ;; [directory "kolab-resources"] type = ldap host = "ldap://localhost:389" bind_dn = "uid=kolab-service,ou=Special Users,dc=example,dc=org" bind_pw = "SomePassword" base_dn = "ou=Resources,dc=example,dc=org" filter = "(&(objectClass=kolabsharedfolder)(kolabfoldertype=event)(mail=%s))" attributes = mail, kolabtargetfolder primary_domain = "example.org" ; Use the Free/Busy daemon that separates the abuse of credentials ;fbsource = "fbdaemon://localhost:?folder=%kolabtargetfolder" ;timeout = 10 ; abort after 10 seconds fbsource = "imap://cyrus-admin:CyrusAdminPassword@imap.lhm.klab.cc/%kolabtargetfolder?acl=lrs" cacheto = /var/cache/kolab-freebusy/%mail.ifb expires = 10m loglevel = 300 ;; ;; For collections, aggregate the free/busy data from all its members ;; [directory "kolab-resource-collections"] type = ldap host = "ldap://localhost:389" bind_dn = "uid=kolab-service,ou=Special Users,dc=example,dc=org" bind_pw = "SomePassword" base_dn = "ou=Resources,dc=example,dc=org" filter = "(&(objectClass=kolabgroupofuniquenames)(mail=%s))" attributes = uniquemember, mail resolve_dn = uniquemember resolve_attribute = mail ; the 'aggregate' source takes one parameter ; denoting the attribute holding all member email addresses fbsource = "aggregate://%uniquemember" ; consider these directories for getting the member's free/busy data directories = kolab-resources cacheto = /var/cache/kolab-freebusy/%mail.ifb expires = 10m loglevel = 200 ; Info diff --git a/lib/Kolab/FreeBusy/DirectoryLDAP.php b/lib/Kolab/FreeBusy/DirectoryLDAP.php index c97340a..e3704fa 100644 --- a/lib/Kolab/FreeBusy/DirectoryLDAP.php +++ b/lib/Kolab/FreeBusy/DirectoryLDAP.php @@ -1,158 +1,168 @@ * * Copyright (C) 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 . */ namespace Kolab\FreeBusy; // PEAR modules operate in global namespace use \Net_LDAP3; use \Kolab\Config; use \Monolog\Logger as Monolog; /** * Implementation of an address lookup using an LDAP directory */ class DirectoryLDAP extends Directory { private $ldap; private $logger; private $ready = false; /** * Default constructor loading directory configuration */ public function __construct($config) { $this->config = $config; $host = parse_url($config['host']); $ldap_config = array( 'hosts' => array($host['host']), 'port' => $host['port'] ?: 389, 'use_tls' => $host['scheme'] == 'tls' || $host['scheme'] == 'ldaps', - 'root_dn' => $config['base_dn'], + 'root_dn' => $config['root_dn'] ?: $config['base_dn'], 'log_hook' => array($this, 'log'), ) + $config; // instantiate Net_LDAP3 and connect with logger $this->logger = Logger::get('ldap', intval($config['loglevel'])); $this->ldap = new Net_LDAP3($ldap_config); // connect + bind to LDAP server if ($this->ldap->connect()) { $this->ready = $this->ldap->bind($config['bind_dn'], $config['bind_pw']); } if ($this->ready) { $this->logger->addInfo("Connected to $config[host] with '$config[bind_dn]'"); } else { $this->logger->addWarning("Connectiion to $config[host] with '$config[bind_dn]' failed!"); } } /** * Callback for Net_LDAP3 logging */ public function log($level, $msg) { // map PHP log levels to Monolog levels static $loglevels = array( LOG_DEBUG => Monolog::DEBUG, LOG_NOTICE => Monolog::NOTICE, LOG_INFO => Monolog::INFO, LOG_WARNING => Monolog::WARNING, LOG_ERR => Monolog::ERROR, LOG_CRIT => Monolog::CRITICAL, LOG_ALERT => Monolog::ALERT, LOG_EMERG => Monolog::EMERGENCY, ); $msg = is_array($msg) ? join('; ', $msg) : strval($msg); $this->logger->addRecord($loglevels[$level], $msg); } /** * @see Directory::resolve() */ public function resolve($user) { $result = array('s' => $user); if ($this->ready) { - // search with configured base_dn and filter + // extract domain name list($u, $d) = explode('@', $user); if (empty($d)) $d = $this->config['primary_domain']; - $replaces = array('%dc' => 'dc=' . str_replace('.', ',dc=', $d), '%u' => $u); + + // resolve domain root dn + if (!empty($this->config['domain_filter'])) { + $dc = $this->ldap->domain_root_dn($d); + } + else { + $dc = 'dc=' . str_replace('.', ',dc=', $d); + } + + // search with configured base_dn and filter + $replaces = array('%dc' => $dc, '%u' => $u); $base_dn = strtr($this->config['base_dn'], $replaces); $filter = str_replace('%s', Net_LDAP3::quote_string($user), strtr($this->config['filter'], $replaces)); $ldapresult = $this->ldap->search($base_dn, $filter, 'sub', Config::convert($this->config['attributes'], Config::ARR)); // got a valid result if ($ldapresult && $ldapresult->count()) { $ldapresult->rewind(); $entry = Net_LDAP3::normalize_entry($ldapresult->current()); // get the first entry $this->logger->addInfo("Found " . $ldapresult->count() . " entries for $filter", $entry); // convert entry attributes to strings and add them to the final result hash array $result += self::_compact_entry($entry); // resolve DN attribute into the actual record if (!empty($this->config['resolve_dn']) && array_key_exists($this->config['resolve_dn'], $result)) { $k = $this->config['resolve_dn']; $member_attr = $this->config['resolve_attribute'] ?: 'mail'; foreach ((array)$result[$k] as $i => $member_dn) { if ($member_rec = $this->ldap->get_entry($member_dn, array($member_attr))) { $member_rec = self::_compact_entry(Net_LDAP3::normalize_entry($member_rec)); $result[$k][$i] = $member_rec[$member_attr]; } } } return $result; } $this->logger->addInfo("No entry found for $filter"); } return false; } /** * Helper method to convert entry attributes to simple values */ private static function _compact_entry($entry) { $result = array(); foreach ($entry as $k => $v) { if (is_array($v) && count($v) > 1) { $result[$k] = array_map('strval', $v); } else if (!empty($v)) { $result[$k] = strval(is_array($v) ? $v[0] : $v); } } return $result; } }