| +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | | Author: Jeroen van Meeuwen | +--------------------------------------------------------------------------+ */ require_once "Net/LDAP3.php"; /** * Kolab LDAP handling abstraction class. */ class LDAP extends Net_LDAP3 { private $conf; /** * Class constructor */ public function __construct($domain = null) { parent::__construct(); $this->conf = Conf::get_instance(); // Causes nesting levels to be too deep...? //$this->config_set('config_get_hook', array($this, "_config_get")); $this->config_set("debug", Log::mode() == Log::TRACE); $this->config_set("log_hook", array($this, "_log")); $this->config_set("config_root_dn", "cn=config"); $this->config_set("service_bind_dn", $this->conf->get("service_bind_dn")); $this->config_set("service_bind_pw", $this->conf->get("service_bind_pw")); $this->config_set("login_filter", $this->conf->get("kolab_wap", "login_filter")); $this->config_set("vlv", $this->conf->get("ldap", "vlv", Conf::AUTO)); // See if we are to connect to any domain explicitly defined. if (empty($domain)) { // If not, attempt to get the domain from the session. if (isset($_SESSION['user'])) { try { $domain = $_SESSION['user']->get_domain(); } catch (Exception $e) { $this->_log(LOG_WARNING, "LDAP: User not authenticated yet"); } } } else { $this->_log(LOG_DEBUG, "LDAP: __construct() using domain $domain"); } // Continue and default to the primary domain. $this->domain = $domain ? $domain : $this->conf->get('primary_domain'); $unique_attr = $this->conf->get($domain, 'unique_attribute'); if (empty($unique_attr)) { $unique_attr = $this->conf->get('ldap', 'unique_attribute'); } if (empty($unique_attr)) { $unique_attr = 'nsuniqueid'; } $this->config_set('unique_attribute', $unique_attr); $this->_ldap_uri = $this->conf->get('ldap_uri'); $this->_ldap_server = parse_url($this->_ldap_uri, PHP_URL_HOST); $this->_ldap_port = parse_url($this->_ldap_uri, PHP_URL_PORT); $this->_ldap_scheme = parse_url($this->_ldap_uri, PHP_URL_SCHEME); // Catch cases in which the ldap server port has not been explicitely defined if (!$this->_ldap_port) { if ($this->_ldap_scheme == "ldaps") { $this->_ldap_port = 636; } else { $this->_ldap_port = 389; } } $this->config_set("host", $this->_ldap_server); $this->config_set("port", $this->_ldap_port); $this->config_set("use_tls", $this->_ldap_scheme == 'tls'); parent::connect(); // Attempt to get the root dn from the configuration file. $root_dn = $this->conf->get($this->domain, "base_dn"); if (empty($root_dn)) { // Fall back to a root dn from LDAP, or the standard root dn $root_dn = $this->domain_root_dn($this->domain); } $this->config_set("root_dn", $root_dn); } /********************************************************** *********** Public methods ************ **********************************************************/ /** * Authentication * * @param string $username User name (DN or mail) * @param string $password User password * * @return bool|string User ID or False on failure */ public function authenticate($username, $password, $domain = NULL) { $this->_log(LOG_DEBUG, "Auth::LDAP: authentication request for $username against domain $domain"); if (!$this->connect()) { return false; } if ($domain == NULL) { $domain = $this->domain; } $result = $this->login($username, $password, $domain); if (!$result) { return false; } $_SESSION['user']->user_bind_dn = $result; $_SESSION['user']->user_bind_pw = $password; return $result; } public function domain_add($domain, $attributes = array()) { if (empty($domain)) { return false; } $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw); $domain_base_dn = $this->conf->get('ldap', 'domain_base_dn'); $domain_name_attribute = $this->conf->get('ldap', 'domain_name_attribute'); $service_bind_dn = $this->conf->get('ldap', 'service_bind_dn'); $primary_domain = $this->conf->get('kolab', 'primary_domain'); if (empty($service_bind_dn)) { $service_bind_dn = $this->conf->get('ldap', 'bind_dn'); } if (empty($domain_name_attribute)) { $domain_name_attribute = 'associateddomain'; } if (!is_array($attributes[$domain_name_attribute])) { $attributes[$domain_name_attribute] = (array) $attributes[$domain_name_attribute]; } if (!in_array($domain, $attributes[$domain_name_attribute])) { array_unshift($attributes[$domain_name_attribute], $domain); } $domain_dn = $domain_name_attribute . '=' . $domain . ',' . $domain_base_dn; $result = $this->add_entry($domain_dn, $attributes); if (!$result) { return false; } if (!empty($attributes['inetdomainbasedn'])) { $inetdomainbasedn = $attributes['inetdomainbasedn']; } else { $inetdomainbasedn = $this->_standard_root_dn($domain); } // Query the ACI for the primary domain if ($domain_entry = $this->_find_domain($primary_domain)) { $domain_entry = array_shift($domain_entry); if (in_array('inetdomainbasedn', $domain_entry)) { $_base_dn = $domain_entry['inetdomainbasedn']; } } if (empty($_base_dn)) { $_base_dn = $this->_standard_root_dn($primary_domain); } $result = $this->_read($_base_dn, array('aci')); $result = $result[key($result)]; $acis = $result['aci']; // Skip one particular ACI foreach ($acis as $aci) { if (stristr($aci, "SIE Group") === false) { continue; } $_aci = $aci; } // @TODO: this list should be configurable or auto-created somehow $self_attrs = array( 'carLicense', 'description', 'displayName', 'facsimileTelephoneNumber', 'homePhone', 'homePostalAddress', 'initials', 'jpegPhoto', 'l', 'labeledURI', 'mobile', 'o', 'pager', 'photo', 'postOfficeBox', 'postalAddress', 'postalCode', 'preferredDeliveryMethod', 'preferredLanguage', 'registeredAddress', 'roomNumber', 'secretary', 'seeAlso', 'st', 'street', 'telephoneNumber', 'telexNumber', 'title', 'userCertificate', 'userPassword', 'userSMIMECertificate', 'x500UniqueIdentifier', ); if (in_array('kolabInetOrgPerson', $this->classes_allowed())) { $self_attrs = array_merge($self_attrs, array('kolabDelegate', 'kolabInvitationPolicy', 'kolabAllowSMTPSender')); } $_domain = str_replace('.', '_', $domain); $dn = $inetdomainbasedn; $cn = str_replace(array(',', '='), array('\2C', '\3D'), $dn); // Additional domain entries in various trees $entries = array( "cn={$cn},cn=mapping tree,cn=config" => array( 'objectclass' => array( 'top', 'extensibleObject', 'nsMappingTree', ), 'nsslapd-state' => 'backend', 'cn' => $inetdomainbasedn, 'nsslapd-backend' => $_domain, ), "cn={$_domain},cn=ldbm database,cn=plugins,cn=config" => array( 'objectclass' => array( 'top', 'extensibleobject', 'nsbackendinstance', ), 'cn' => $_domain, 'nsslapd-suffix' => $inetdomainbasedn, 'nsslapd-cachesize' => '-1', 'nsslapd-cachememsize' => '10485760', 'nsslapd-readonly' => 'off', 'nsslapd-require-index' => 'off', 'nsslapd-dncachememsize' => '10485760', 'nsslapd-directory' => true, // will be replaced below ), $inetdomainbasedn => array( // @TODO: Probably just use ldap_explode_dn() 'dc' => substr($dn, (strpos($dn, '=')+1), ((strpos($dn, ',')-strpos($dn, '='))-1)), 'objectclass' => array( 'top', 'domain', ), 'aci' => array( // Self-modification "(targetattr = \"" . implode(" || ", $self_attrs) . "\")(version 3.0; acl \"Enable self write for common attributes\"; allow (read,compare,search,write) userdn=\"ldap:///self\";)", // Directory Administrators "(targetattr = \"*\")(version 3.0; acl \"Directory Administrators Group\"; allow (all) (groupdn=\"ldap:///cn=Directory Administrators,{$inetdomainbasedn}\" or roledn=\"ldap:///cn=kolab-admin,{$inetdomainbasedn}\");)", // Configuration Administrators "(targetattr = \"*\")(version 3.0; acl \"Configuration Administrators Group\"; allow (all) groupdn=\"ldap:///cn=Configuration Administrators,ou=Groups,ou=TopologyManagement,o=NetscapeRoot\";)", // Administrator users "(targetattr = \"*\")(version 3.0; acl \"Configuration Administrator\"; allow (all) userdn=\"ldap:///uid=admin,ou=Administrators,ou=TopologyManagement,o=NetscapeRoot\";)", // SIE Group $_aci, // Search Access, "(targetattr != \"userPassword\") (version 3.0; acl \"Search Access\"; allow (read,compare,search) (userdn = \"ldap:///{$inetdomainbasedn}??sub?(objectclass=*)\");)", // Service Search Access "(targetattr = \"*\") (version 3.0; acl \"Service Search Access\"; allow (read,compare,search) (userdn = \"ldap:///{$service_bind_dn}\");)", ), ), ); $replica_hosts = $this->list_replicas(); if (!empty($replica_hosts)) { foreach ($replica_hosts as $replica_host) { $ldap = new Net_LDAP3($this->config); $ldap->config_set("log_hook", array($this, "_log")); $ldap->config_set('hosts', array($replica_host)); $ldap->connect(); $ldap->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw); foreach ($entries as $dn => $attrs) { if (isset($attrs['nsslapd-directory'])) { $attrs['nsslapd-directory'] = $this->nsslapd_directory($ldap, $domain); } if (!$ldap->add_entry($dn, $attrs)) { $this->_log(LOG_ERR, "Error adding $dn to $replica_host"); } } $ldap->close(); } } else { foreach ($entries as $dn => $attrs) { if (isset($attrs['nsslapd-directory'])) { $attrs['nsslapd-directory'] = $this->nsslapd_directory($this, $domain); } if (!$this->add_entry($dn, $attrs)) { $this->_log(LOG_ERR, "Error adding $dn"); } } } if (!empty($replica_hosts)) { $this->add_replication_agreements($inetdomainbasedn); } // add OUs, do this after adding replication agreements $entries = array( "cn=Directory Administrators,$inetdomainbasedn" => array( 'cn' => 'Directory Administrators', 'objectclass' => array('top', 'groupofuniquenames'), 'uniquemember' => array('cn=Directory Manager'), ), "cn=kolab-admin,$inetdomainbasedn" => array( 'cn' => 'kolab-admin', 'objectclass' => array( 'top', 'ldapsubentry', 'nsroledefinition', 'nssimpleroledefinition', 'nsmanagedroledefinition', ), ), // @TODO: these OUs DN should be read from config "ou=Groups,$inetdomainbasedn" => array( 'ou' => 'Groups', 'objectclass' => array('top', 'organizationalunit'), ), "ou=People,$inetdomainbasedn" => array( 'ou' => 'People', 'objectclass' => array('top', 'organizationalunit'), ), "ou=Special Users,$inetdomainbasedn" => array( 'ou' => 'Special Users', 'objectclass' => array('top', 'organizationalunit'), ), "ou=Resources,$inetdomainbasedn" => array( 'ou' => 'Resources', 'objectclass' => array('top', 'organizationalunit'), ), "ou=Shared Folders,$inetdomainbasedn" => array( 'ou' => 'Shared Folders', 'objectclass' => array('top', 'organizationalunit'), ), ); // create set of OUs and other domain entries foreach ($entries as $dn => $attrs) { $this->add_entry($dn, $attrs); } return $domain_dn; } public function domain_edit($domain, $attributes, $typeid = null) { $domain = $this->domain_info($domain, array_keys($attributes)); if (empty($domain)) { return false; } $domain_dn = key($domain); // We should start throwing stuff over the fence here. return $this->modify_entry($domain_dn, $domain[$domain_dn], $attributes); } public function domain_delete($domain) { $domain = $this->domain_info($domain); if (empty($domain)) { return false; } $domain_dn = key($domain); $attributes = array_merge($domain[$domain_dn], array('inetdomainstatus' => 'deleted')); // for performance reasons we set only domain status, // cronjob script should delete such domain later return $this->modify_entry($domain_dn, $domain[$domain_dn], $attributes); } public function domain_find_by_attribute($attribute) { $base_dn = $this->conf->get('ldap', 'domain_base_dn'); return $this->entry_find_by_attribute($attribute, $base_dn); } public function domain_info($domain, $attributes = array('*')) { $this->_log(LOG_DEBUG, "Auth::LDAP::domain_info() for domain " . var_export($domain, true)); if (empty($domain)) { return false; } $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw); $domain_base_dn = $this->conf->get('ldap', 'domain_base_dn'); $domain_dn = $this->entry_dn($domain, array(), $domain_base_dn); if (!$domain_dn) { $result = $this->_find_domain($domain, $attributes); } else { $result = $this->_read($domain_dn, $attributes); } $this->_log(LOG_DEBUG, "Auth::LDAP::domain_info() result: " . var_export($result, true)); return $result ? $result : false; } /** * Checkes if specified domain is empty (no users assigned) * * @param string $domain Domain name * * @return bool True if domain is empty, False otherwise */ public function domain_is_empty($domain) { $this->_log(LOG_DEBUG, "Auth::LDAP::domain_is_empty($domain)"); $domain_name_attribute = $this->conf->get('ldap', 'domain_name_attribute'); if (empty($domain_name_attribute)) { $domain_name_attribute = 'associateddomain'; } $domain = $this->domain_info($domain); if (!empty($domain)) { $domain_dn = key($domain); $domain_name = $domain[$domain_dn][$domain_name_attribute]; } else { return false; } $result = $this->list_users(array('entrydn'), null, array('page_size' => 1), $domain_name); return is_array($result) && $result['count'] == 0; } /** * Proxy to parent function in order to enable us to insert our * configuration. */ public function effective_rights($subject) { $ckey = $_SESSION['user']->user_bind_dn . '#' . md5($this->domain . '::' . $subject . '::' . $_SESSION['user']->user_bind_pw); // use memcache if ($result = $this->get_cache_data($ckey)) { return $result; } // use internal cache else if (isset($this->icache[$ckey])) { return $this->icache[$ckey]; } // Ensure we are bound with the user's credentials $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw); $this->_log(LOG_DEBUG, "Auth::LDAP::effective_rights(\$subject = '" . $subject . "')"); switch ($subject) { case "domain": $result = parent::effective_rights($this->conf->get("ldap", "domain_base_dn")); break; case "group": case "ou": case "resource": case "role": case "sharedfolder": case "user": $result = parent::effective_rights($this->_subject_base_dn($subject)); break; default: $result = parent::effective_rights($subject); } if (!$result) { $result = $this->legacy_rights($subject); } if (!$this->set_cache_data($ckey, $result)) { $this->icache[$ckey] = $result; } return $result; } public function find_recipient($address) { if (strpos($address, '@') === false) { return false; } $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw); $root_dn = $this->config_get('root_dn'); $mail_attrs = $this->conf->get_list('mail_attributes') ?: array('mail', 'alias'); $search = array('operator' => 'OR'); foreach ($mail_attrs as $num => $attr) { $search['params'][$attr] = array( 'type' => 'exact', 'value' => $address, ); } $result = $this->search_entries($root_dn, '(objectclass=*)', 'sub', $mail_attrs, array('search' => $search)); if ($result && $result->count() > 0) { return $result->entries(true); } return false; } public function get_attributes($subject_dn, $attributes) { $this->_log(LOG_DEBUG, "Auth::LDAP::get_attributes() for $subject_dn"); $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw); return $this->get_entry_attributes($subject_dn, $attributes); } public function group_add($attrs, $typeid = null) { if (!empty($attrs['ou'])) { $base_dn = $attrs['ou']; unset($attrs['ou']); } else { $base_dn = $this->entry_base_dn('group', $typeid); } // TODO: The rdn is configurable as well. // Use [$type_str . "_"]user_rdn_attr $dn = "cn=" . $attrs['cn'] . "," . $base_dn; return $this->entry_add($dn, $attrs); } public function group_delete($group) { return $this->entry_delete($group); } public function group_edit($group, $attributes, $typeid = null) { $group = $this->group_info($group, array_keys($attributes)); if (empty($group)) { return false; } $group_dn = key($group); // We should start throwing stuff over the fence here. return $this->modify_entry($group_dn, $group[$group_dn], $attributes); } public function group_find_by_attribute($attribute) { return $this->entry_find_by_attribute($attribute); } public function group_info($group, $attributes = array('*')) { $this->_log(LOG_DEBUG, "Auth::LDAP::group_info() for group " . var_export($group, true)); $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw); $group_dn = $this->entry_dn($group); if (!$group_dn) { return false; } $this->read_prepare($attributes); return $this->_read($group_dn, $attributes); } public function group_members_list($group, $recurse = true) { $group_dn = $this->entry_dn($group); if (!$group_dn) { return false; } return $this->list_group_members($group_dn, null, $recurse); } public function list_domains($attributes = array(), $search = array(), $params = array()) { $this->_log(LOG_DEBUG, "Auth::LDAP::list_domains(" . var_export($attributes, true) . ", " . var_export($search, true) . ", " . var_export($params, true)); $section = $this->conf->get('kolab', 'auth_mechanism'); $base_dn = $this->conf->get($section, 'domain_base_dn'); $filter = $this->conf->get($section, 'domain_filter'); $kolab_filter = $this->conf->get($section, 'kolab_domain_filter'); if (empty($filter) && !empty($kolab_filter)) { $filter = $kolab_filter; } if (!$filter) { $filter = "(associateddomain=*)"; } return $this->_list($base_dn, $filter, 'sub', $attributes, $search, $params); } public function list_groups($attributes = array(), $search = array(), $params = array()) { $this->_log(LOG_DEBUG, "Auth::LDAP::list_groups(" . var_export($attributes, true) . ", " . var_export($search, true) . ", " . var_export($params, true)); $base_dn = $this->_subject_base_dn('group'); $filter = $this->conf->get('group_filter'); if (!$filter) { $filter = "(|(objectclass=groupofuniquenames)(objectclass=groupofurls))"; } return $this->_list($base_dn, $filter, 'sub', $attributes, $search, $params); } public function list_organizationalunits($attributes = array(), $search = array(), $params = array()) { $this->_log(LOG_DEBUG, "Auth::LDAP::list_organizationalunits(" . var_export($attributes, true) . ", " . var_export($search, true) . ", " . var_export($params, true)); $base_dn = $this->_subject_base_dn('ou'); $filter = $this->conf->get('ou_filter'); if (!$filter) { $filter = "(objectclass=organizationalunit)"; } return $this->_list($base_dn, $filter, 'sub', $attributes, $search, $params); } public function list_resources($attributes = array(), $search = array(), $params = array()) { $this->_log(LOG_DEBUG, "Auth::LDAP::list_resources(" . var_export($attributes, true) . ", " . var_export($search, true) . ", " . var_export($params, true)); $base_dn = $this->_subject_base_dn('resource'); $filter = $this->conf->get('resource_filter'); if (!$filter) { $filter = "(&(objectclass=*)(!(objectclass=organizationalunit)))"; } return $this->_list($base_dn, $filter, 'sub', $attributes, $search, $params); } public function list_roles($attributes = array(), $search = array(), $params = array()) { $this->_log(LOG_DEBUG, "Auth::LDAP::list_roles(" . var_export($attributes, true) . ", " . var_export($search, true) . ", " . var_export($params, true)); $base_dn = $this->_subject_base_dn('role'); $filter = $this->conf->get('role_filter'); if (empty($filter)) { $filter = "(&(objectclass=ldapsubentry)(objectclass=nsroledefinition))"; } return $this->_list($base_dn, $filter, 'sub', $attributes, $search, $params); } public function list_sharedfolders($attributes = array(), $search = array(), $params = array()) { $this->_log(LOG_DEBUG, "Auth::LDAP::list_sharedfolders(" . var_export($attributes, true) . ", " . var_export($search, true) . ", " . var_export($params, true)); $base_dn = $this->_subject_base_dn('sharedfolder'); $filter = $this->conf->get('sharedfolder_filter'); if (!$filter) { $filter = "(&(objectclass=*)(!(objectclass=organizationalunit)))"; } return $this->_list($base_dn, $filter, 'sub', $attributes, $search, $params); } public function list_users($attributes = array(), $search = array(), $params = array(), $domain = null) { $this->_log(LOG_DEBUG, "Auth::LDAP::list_users(" . var_export($attributes, true) . ", " . var_export($search, true) . ", " . var_export($params, true) . ", " . $domain . ")"); $base_dn = $this->_subject_base_dn('user', false, $domain); $filter = $this->conf->get('user_filter'); if (empty($filter)) { $filter = "(objectclass=kolabinetorgperson)"; } return $this->_list($base_dn, $filter, 'sub', $attributes, $search, $params); } public function organizationalunit_add($attrs, $typeid = null) { if (!empty($attrs['base_dn'])) { $base_dn = $attrs['base_dn']; unset($attrs['base_dn']); } else { $base_dn = $this->entry_base_dn('ou', $typeid); } // TODO: The rdn is configurable as well. // Use [$type_str . "_"]ou_rdn_attr $dn = "ou=" . $attrs['ou'] . "," . $base_dn; return $this->entry_add($dn, $attrs); } public function organizationalunit_edit($ou, $attributes, $typeid = null) { $ou = $this->organizationalunit_info($ou, array_keys($attributes)); if (empty($ou)) { return false; } $dn = key($ou); // We should start throwing stuff over the fence here. return $this->modify_entry($dn, $ou[$dn], $attributes); } public function organizationalunit_delete($ou) { return $this->entry_delete($ou, array('objectclass' => 'organizationalunit')); } public function organizationalunit_find_by_attribute($attribute) { $attribute['objectclass'] = 'organizationalunit'; return $this->entry_find_by_attribute($attribute); } public function organizationalunit_info($ou, $attributes = array('*')) { $this->_log(LOG_DEBUG, "Auth::LDAP::organizationalunit_info() for unit " . var_export($ou, true)); $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw); $dn = $this->entry_dn($ou, array('objectclass' => 'organizationalunit')); if (!$dn) { return false; } $this->read_prepare($attributes); return $this->_read($dn, $attributes); } public function resource_add($attrs, $typeid = null) { if (!empty($attrs['ou'])) { $base_dn = $attrs['ou']; unset($attrs['ou']); } else { $base_dn = $this->entry_base_dn('resource', $typeid); } // TODO: The rdn is configurable as well. // Use [$type_str . "_"]resource_rdn_attr $dn = "cn=" . $attrs['cn'] . "," . $base_dn; return $this->entry_add($dn, $attrs); } public function resource_delete($resource) { return $this->entry_delete($resource); } public function resource_edit($resource, $attributes, $typeid = null) { $resource = $this->resource_info($resource, array_keys($attributes)); if (empty($resource)) { return false; } $resource_dn = key($resource); // We should start throwing stuff over the fence here. return $this->modify_entry($resource_dn, $resource[$resource_dn], $attributes); } public function resource_find_by_attribute($attribute) { return $this->entry_find_by_attribute($attribute); } public function resource_info($resource, $attributes = array('*')) { $this->_log(LOG_DEBUG, "Auth::LDAP::resource_info() for resource " . var_export($resource, true)); $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw); $resource_dn = $this->entry_dn($resource); if (!$resource_dn) { return false; } $this->read_prepare($attributes); return $this->_read($resource_dn, $attributes); } public function role_add($attrs, $typeid = null) { $base_dn = $this->entry_base_dn('role', $typeid); // TODO: The rdn is configurable as well. // Use [$type_str . "_"]role_rdn_attr $dn = "cn=" . $attrs['cn'] . "," . $base_dn; return $this->entry_add($dn, $attrs); } public function role_edit($role, $attributes, $typeid = null) { $role = $this->role_info($role, array_keys($attributes)); if (empty($role)) { return false; } $role_dn = key($role); // We should start throwing stuff over the fence here. return $this->modify_entry($role_dn, $role[$role_dn], $attributes); } public function role_delete($role) { return $this->entry_delete($role, array('objectclass' => 'ldapsubentry')); } public function role_find_by_attribute($attribute) { $attribute['objectclass'] = 'ldapsubentry'; return $this->entry_find_by_attribute($attribute); } public function role_info($role, $attributes = array('*')) { $this->_log(LOG_DEBUG, "Auth::LDAP::role_info() for role " . var_export($role, true)); $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw); $role_dn = $this->entry_dn($role, array('objectclass' => 'ldapsubentry')); if (!$role_dn) { return false; } $this->read_prepare($attributes); return $this->_read($role_dn, $attributes); } public function sharedfolder_add($attrs, $typeid = null) { $base_dn = $this->entry_base_dn('sharedfolder', $typeid); // TODO: The rdn is configurable as well. // Use [$type_str . "_"]user_rdn_attr $dn = "cn=" . $attrs['cn'] . "," . $base_dn; return $this->entry_add($dn, $attrs); } public function sharedfolder_delete($sharedfolder) { return $this->entry_delete($sharedfolder); } public function sharedfolder_edit($sharedfolder, $attributes, $typeid = null) { $sharedfolder = $this->sharedfolder_info($sharedfolder, array_keys($attributes)); if (empty($sharedfolder)) { return false; } $sharedfolder_dn = key($sharedfolder); // We should start throwing stuff over the fence here. return $this->modify_entry($sharedfolder_dn, $sharedfolder[$sharedfolder_dn], $attributes); } public function sharedfolder_find_by_attribute($attribute) { return $this->entry_find_by_attribute($attribute); } public function sharedfolder_info($sharedfolder, $attributes = array('*')) { $this->_log(LOG_DEBUG, "Auth::LDAP::sharedfolder_info() for sharedfolder " . var_export($sharedfolder, true)); $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw); $sharedfolder_dn = $this->entry_dn($sharedfolder); if (!$sharedfolder_dn) { return false; } $this->read_prepare($attributes); return $this->_read($sharedfolder_dn, $attributes); } public function search($base_dn, $filter = '(objectclass=*)', $scope = 'sub', $sort = NULL, $search = array()) { if (isset($_SESSION['user']->user_bind_dn) && !empty($_SESSION['user']->user_bind_dn)) { $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw); } $this->_log(LOG_DEBUG, "Relaying search to parent:" . var_export(func_get_args(), true)); return parent::search($base_dn, $filter, $scope, $sort, $search); } public function subject_base_dn($subject, $strict = false) { return $this->_subject_base_dn($subject, $strict); } public function user_add($attrs, $typeid = null) { $base_dn = $this->entry_base_dn('user', $typeid); if (!empty($attrs['ou'])) { $base_dn = $attrs['ou']; unset($attrs['ou']); } // TODO: The rdn is configurable as well. // Use [$type_str . "_"]user_rdn_attr $dn = "uid=" . $attrs['uid'] . "," . $base_dn; return $this->entry_add($dn, $attrs); } public function user_edit($user, $attributes, $typeid = null) { $user = $this->user_info($user, array_keys($attributes)); if (empty($user)) { return false; } $user_dn = key($user); // We should start throwing stuff over the fence here. $result = $this->modify_entry($user_dn, $user[$user_dn], $attributes); // Handle modification of current user data if (!empty($result) && $user_dn == $_SESSION['user']->user_bind_dn) { // update session password if (!empty($result['replace']) && !empty($result['replace']['userpassword'])) { $pass = $result['replace']['userpassword']; $_SESSION['user']->user_bind_pw = is_array($pass) ? implode($pass) : $pass; } } return $result; } public function user_delete($user) { return $this->entry_delete($user); } public function user_info($user, $attributes = array('*')) { $this->_log(LOG_DEBUG, "Auth::LDAP::user_info() for user " . var_export($user, true)); $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw); $user_dn = $this->entry_dn($user); if (!$user_dn) { return false; } $this->read_prepare($attributes); return $this->_read($user_dn, $attributes); } public function user_find_by_attribute($attribute) { return $this->entry_find_by_attribute($attribute); } /** * Returns attributes available in specified object classes */ public function attributes_allowed($objectclasses = array()) { $attributes = parent::attributes_allowed($objectclasses); // additional special attributes that aren't in LDAP schema $additional_attributes = array( 'top' => array('nsRoleDN'), '*' => array('aci'), ); if (!empty($attributes)) { foreach ($additional_attributes as $class => $attrs) { if (in_array($class, $objectclasses)) { $attributes['may'] = array_merge($attributes['may'], $attrs); } } $attributes['may'] = array_merge($attributes['may'], $additional_attributes['*']); } return $attributes; } /** * Wrapper for search_entries() */ protected function _list($base_dn, $filter, $scope, $attributes, $search, $params) { $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw); if (!empty($params['sort_by'])) { if (is_array($params['sort_by'])) { foreach ($params['sort_by'] as $attrib) { if (!in_array($attrib, $attributes)) { $attributes[] = $attrib; } } } else { if (!in_array($params['sort_by'], $attributes)) { $attributes[] = $params['sort_by']; } } } if (!empty($params['page_size'])) { $this->config_set('page_size', $params['page_size']); } else { $this->config_set('page_size', 15); } if (!empty($params['page'])) { $this->config_set('list_page', $params['page']); } else { $this->config_set('list_page', 1); } if (empty($attributes) || !is_array($attributes)) { $attributes = array('*'); } $result = $this->search_entries($base_dn, $filter, $scope, $attributes, array('search' => $search)); $entries = $this->sort_and_slice($result, $params); return array( 'list' => $entries, 'count' => is_object($result) ? $result->count() : 0, ); } /** * Prepare environment before _read() call */ protected function read_prepare(&$attributes) { // always return unique attribute $unique_attr = $this->conf->get('unique_attribute'); if (empty($unique_attr)) { $unique_attr = 'nsuniqueid'; } $this->_log(LOG_NOTICE, "Using unique_attribute " . var_export($unique_attr, TRUE) . " at " . __FILE__ . ":" . __LINE__); if (!in_array($unique_attr, $attributes)) { $attributes[] = $unique_attr; } } /** * delete_entry() wrapper with binding and DN resolving */ protected function entry_delete($entry, $attributes = array(), $base_dn = null) { $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw); $entry_dn = $this->entry_dn($entry, $attributes, $base_dn); // object not found or self deletion if (!$entry_dn || $entry_dn == $_SESSION['user']->user_bind_dn) { return false; } return $this->delete_entry($entry_dn); } /** * add_entry() wrapper with binding */ protected function entry_add($entry_dn, $attrs) { $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw); if ($this->add_entry($entry_dn, $attrs)) { return $entry_dn; } return false; } /** * Return base DN for specified object type */ protected function entry_base_dn($type, $typeid = null) { if ($typeid) { $db = SQL::get_instance(); $query = $db->query("SELECT " . $db->quote_identifier('key') . " FROM {$type}_types WHERE id = ?", array($typeid)); $sql = $db->fetch_assoc($query); // Check if the type has a specific base DN specified. $base_dn = $this->_subject_base_dn($sql['key'] . '_' . $type, true); } if (empty($base_dn)) { $base_dn = $this->_subject_base_dn($type); } return $base_dn; } public function _config_get($key, $default = NULL) { $key_parts = explode("_", $key); $this->_log(LOG_DEBUG, var_export($key_parts)); while (!empty($key_parts)) { $value = $this->conf->get(implode("_", $key_parts)); if (empty($value)) { $_discard = array_shift($key_parts); } else { break; } } if (empty($value)) { return $default; } else { return $value; } } public function _log($level, $msg) { if (strstr($_SERVER["REQUEST_URI"], "/api/")) { $str = "(api) "; } else { $str = ""; } if (is_array($msg)) { $msg = implode("\n", $msg); } switch ($level) { case LOG_DEBUG: Log::debug($str . $msg); break; case LOG_ERR: case LOG_ALERT: case LOG_CRIT: case LOG_EMERG: Log::error($str . $msg); break; case LOG_INFO: Log::info($str . $msg); break; case LOG_WARNING: Log::warning($str . $msg); break; case LOG_NOTICE: default: Log::trace($str . $msg); break; } } private function _subject_base_dn($subject, $strict = false, $domain = null) { if (empty($domain)) { $domain = $this->domain; } $subject_base_dn = $this->conf->get_raw($domain, $subject . "_base_dn"); if (empty($subject_base_dn)) { $subject_base_dn = $this->conf->get_raw("ldap", $subject . "_base_dn"); } if (empty($subject_base_dn) && $strict) { $this->_log(LOG_DEBUG, "subject_base_dn for subject $subject not found"); return null; } // Attempt to get a configured base_dn $base_dn = $this->conf->get($domain, "base_dn"); if (empty($base_dn)) { $base_dn = $this->domain_root_dn($domain); } if (!empty($subject_base_dn)) { $base_dn = $this->conf->expand($subject_base_dn, array("base_dn" => $base_dn)); } $this->_log(LOG_DEBUG, "subject_base_dn for subject $subject is $base_dn"); return $base_dn; } private function legacy_rights($subject) { $subject_dn = $this->entry_dn($subject); $user_is_admin = false; $user_is_self = false; // List group memberships $user_groups = $this->find_user_groups($_SESSION['user']->user_bind_dn); console("User's groups", $user_groups); foreach ($user_groups as $user_group_dn) { if ($user_is_admin) continue; $user_group_dn_components = ldap_explode_dn($user_group_dn, 1); unset($user_group_dn_components["count"]); $user_group_cn = array_shift($user_group_dn_components); if (in_array($user_group_cn, array('admin', 'maintainer', 'domain-maintainer'))) { // All rights default to write. $user_is_admin = true; } else { // The user is a regular user, see if the subject is the same has the // user session's bind_dn. if ($subject_dn == $_SESSION['user']->user_bind_dn) { $user_is_self = true; } } } if ($user_is_admin) { $standard_rights = array("add", "delete", "read", "write"); } elseif ($user_is_self) { $standard_rights = array("read", "write"); } else { $standard_rights = array("read"); } $rights = array( 'entryLevelRights' => $standard_rights, 'attributeLevelRights' => array(), ); $subject = $this->_search($subject_dn); if (!$subject) { return $rights; } $subject = $subject->entries(true); $attributes = $this->attributes_allowed($subject[$subject_dn]['objectclass']); $attributes = array_merge((array)$attributes['may'], (array)$attributes['must']); foreach ($attributes as $attribute) { $rights['attributeLevelRights'][$attribute] = $standard_rights; } return $rights; } private function sort_and_slice(&$result, &$params) { if (!is_object($result)) { return array(); } $entries = $result->entries(true); if ($this->vlv_active) { return $entries; } if (!empty($params) && is_array($params)) { if (!empty($params['sort_by'])) { $this->sort_result_key = $params['sort_by']; uasort($entries, array($this, 'sort_result')); } if (!empty($params['sort_order']) && $params['sort_order'] == "DESC") { $entries = array_reverse($entries, true); } if (!empty($params['page_size']) && !empty($params['page'])) { if ($result->count() > $params['page_size']) { $entries = array_slice($entries, (($params['page'] - 1) * $params['page_size']), $params['page_size'], true); } } } return $entries; } /** * Result sorting callback for uasort() */ private function sort_result($a, $b) { if (is_array($this->sort_result_key)) { foreach ($this->sort_result_key as $attrib) { if (array_key_exists($attrib, $a) && !$str1) { $str1 = $a[$attrib]; } if (array_key_exists($attrib, $b) && !$str2) { $str2 = $b[$attrib]; } } } else { $str1 = $a[$this->sort_result_key]; $str2 = $b[$this->sort_result_key]; } if (is_array($str1)) { $str1 = array_shift($str1); } if (is_array($str2)) { $str2 = array_shift($str2); } return strcmp(mb_strtoupper($str1), mb_strtoupper($str2)); } /** * Qualify a username. * * Where username is 'kanarip@kanarip.com', the function will return an * array containing 'kanarip' and 'kanarip.com'. However, where the * username is 'kanarip', the domain name is to be assumed the * management domain name. */ private function _qualify_id($username) { $username_parts = explode('@', $username); if (count($username_parts) == 1) { $domain_name = $this->conf->get('primary_domain'); } else { $domain_name = array_pop($username_parts); } return array(implode('@', $username_parts), $domain_name); } /*********************************************************** ************ Shortcut functions **************** ***********************************************************/ /** * Translate a domain name into it's corresponding root dn. */ private function domain_root_dn($domain) { if (empty($domain)) { return false; } $ckey = 'domain.root::' . $domain; if ($result = $this->icache[$ckey]) { return $result; } if (!$this->connect()) { $this->_log(LOG_DEBUG, "Could not connect"); return false; } $bind_dn = $this->config_get("service_bind_dn", $this->conf->get("service_bind_dn")); $bind_pw = $this->config_get("service_bind_pw", $this->conf->get("service_bind_pw")); if (!$this->bind($bind_dn, $bind_pw)) { return false; } $this->_log(LOG_DEBUG, "Auth::LDAP::domain_root_dn(\$domain = $domain)"); if ($entry_attrs = $this->_find_domain($domain)) { $entry_attrs = array_shift($entry_attrs); $domain_name_attribute = $this->conf->get('ldap', 'domain_name_attribute'); if (empty($domain_name_attribute)) { $domain_name_attribute = 'associateddomain'; } if (is_array($entry_attrs)) { if (array_key_exists('inetdomainbasedn', $entry_attrs) && !empty($entry_attrs['inetdomainbasedn'])) { $domain_root_dn = $entry_attrs['inetdomainbasedn']; } else { if (is_array($entry_attrs[$domain_name_attribute])) { $domain_root_dn = $this->_standard_root_dn($entry_attrs[$domain_name_attribute][0]); } else { $domain_root_dn = $this->_standard_root_dn($entry_attrs[$domain_name_attribute]); } } } } if (empty($domain_root_dn)) { $domain_root_dn = $this->_standard_root_dn($domain); } $this->icache[$ckey] = $domain_root_dn; return $domain_root_dn; } /** * Probe the root dn with the user credentials. * * When a list of domains is retrieved, this does not mean the user * actually has access. Given the root dn for each domain however, we * can in fact attempt to list / search the root dn and see if we get * any results. If we don't, maybe this user is not authorized for the * domain at all? */ private function _probe_root_dn($entry_root_dn) { //console("Running for entry root dn: " . $entry_root_dn); if (($tmpconn = ldapconnect($this->_ldap_server)) == false) { //message("LDAP Error: " . $this->_errstr()); return false; } //console("User DN: " . $_SESSION['user']->user_bind_dn); if (ldap_bind($tmpconn, $_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw) === false) { //message("LDAP Error: " . $this->_errstr()); return false; } if (($list_success = ldap_list($tmpconn, $entry_root_dn, '(objectClass=*)', array('*', 'aci'))) === false) { //message("LDAP Error: " . $this->_errstr()); return false; } return true; } private function _read($entry_dn, $attributes = array('*')) { $result = $this->search($entry_dn, '(objectclass=*)', 'base', $attributes); if ($result) { $this->_log(LOG_DEBUG, "Auth::LDAP::_read() result: " . var_export($result->entries(true), true)); return $result->entries(true); } else { return false; } } private function _search($base_dn, $filter = '(objectclass=*)', $attributes = array('*')) { $result = $this->search($base_dn, $filter, 'sub', $attributes); $this->_log(LOG_DEBUG, "Auth::LDAP::_search on $base_dn with $filter for attributes: " . var_export($attributes, true) . " with result: " . var_export($result, true)); return $result; } /** * Find domain by name * * @param string $domain Domain name * @param array $attributes Result attributes * * @return array Domain records indexed by base DN */ private function _find_domain($domain, $attributes = array('*')) { $this->_log(LOG_DEBUG, "Auth::LDAP::_find_domain($domain)"); $ckey = 'domain::' . $domain; // use memcache if ($domain_dn = $this->get_cache_data($ckey)) { return $this->_read($domain_dn, $attributes); } $domain_base_dn = $this->conf->get('ldap', 'domain_base_dn'); $domain_filter = $this->conf->get('ldap', 'domain_filter'); $domain_name_attribute = $this->conf->get('ldap', 'domain_name_attribute'); if (empty($domain_name_attribute)) { $domain_name_attribute = 'associateddomain'; } $domain_filter = "(&" . $domain_filter . "(" . $domain_name_attribute . "=" . $domain . "))"; if ($result = $this->_search($domain_base_dn, $domain_filter, $attributes)) { $result = $result->entries(true); // cache domain DN if (count($result) == 1) { $this->set_cache_data($ckey, key($result)); } return $result; } } /** * From a domain name, such as 'kanarip.com', create a standard root * dn, such as 'dc=kanarip,dc=com'. * * As the parameter $associatedDomains, either pass it an array (such * as may have been returned by ldap_get_entries() or perhaps * ldap_list()), where the function will assume the first value * ($array[0]) to be the uber-level domain name, or pass it a string * such as 'kanarip.nl'. * * @return string */ private function _standard_root_dn($associatedDomains) { if (is_array($associatedDomains)) { // Usually, the associatedDomain in position 0 is the naming attribute associatedDomain if ($associatedDomains['count'] > 1) { // Issue a debug message here $relevant_associatedDomain = $associatedDomains[0]; } else { $relevant_associatedDomain = $associatedDomains[0]; } } else { $relevant_associatedDomain = $associatedDomains; } return "dc=" . implode(',dc=', explode('.', $relevant_associatedDomain)); } /** * Finds nsslapd-directory for specified domain */ protected function nsslapd_directory($ldap, $domain) { $primary_domain = $this->conf->get('kolab', 'primary_domain'); $_primary_domain = str_replace('.', '_', $primary_domain); $_domain = str_replace('.', '_', $domain); $roots = array($_primary_domain, $primary_domain, 'userRoot'); foreach ($roots as $root) { if ($result = $ldap->get_entry("cn=$root,cn=ldbm database,cn=plugins,cn=config")) { break; } } $this->_log(LOG_DEBUG, "Primary domain ldbm database configuration entry: " . var_export($result, true)); $result = $result[key($result)]; $orig_directory = $result['nsslapd-directory']; $directory = $orig_directory; reset($roots); foreach ($roots as $root) { if ($directory == $orig_directory) { $directory = str_replace($root, $_domain, $result['nsslapd-directory']); } } $this->_log(LOG_DEBUG, "nsslapd-directory for domain $domain is $directory"); return $directory; } /** * Get global handle for memcache access * * @return object Memcache */ public function get_cache() { if (!isset($this->memcache)) { // no memcache support in PHP if (!class_exists('Memcache')) { $this->memcache = false; return false; } // add all configured hosts to pool $pconnect = $this->conf->get('kolab_wap', 'memcache_pconnect', Conf::BOOL); $hosts = $this->conf->get('kolab_wap', 'memcache_hosts'); if ($hosts) { $this->memcache = new Memcache; $this->mc_available = 0; $hosts = explode(',', $hosts); foreach ($hosts as $host) { $host = trim($host); if (substr($host, 0, 7) != 'unix://') { list($host, $port) = explode(':', $host); if (!$port) $port = 11211; } else { $port = 0; } $this->mc_available += intval($this->memcache->addServer( $host, $port, $pconnect, 1, 1, 15, false, array($this, 'memcache_failure'))); } // test connection and failover (will result in $this->mc_available == 0 on complete failure) $this->memcache->increment('__CONNECTIONTEST__', 1); // NOP if key doesn't exist } if (!$this->mc_available) { $this->memcache = false; } } return $this->memcache; } /** * Callback for memcache failure */ public function memcache_failure($host, $port) { static $seen = array(); // only report once if (!$seen["$host:$port"]++) { $this->mc_available--; $this->_log(LOG_ERR, "Memcache failure on host $host:$port"); } } /** * Get cached data * * @param string $key Cache key * * @return mixed Cached value */ public function get_cache_data($key) { if ($cache = $this->get_cache()) { return $cache->get($key); } } /** * Store cached data * * @param string $key Cache key * @param mixed $data Data * @param int $ttl Cache TTL in seconds * * @return bool False on failure or when cache is disabled, True if data was saved succesfully */ public function set_cache_data($key, $data, $ttl = 3600) { if ($cache = $this->get_cache()) { if (!$cache->replace($key, $data, MEMCACHE_COMPRESSED, $ttl)) { return $cache->set($key, $data, MEMCACHE_COMPRESSED, $ttl); } else { return true; } } return false; } }