diff --git a/lib/Auth/LDAP.php b/lib/Auth/LDAP.php index 4bf2a8f..aac6df8 100644 --- a/lib/Auth/LDAP.php +++ b/lib/Auth/LDAP.php @@ -1,1495 +1,1500 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | | Author: Jeroen van Meeuwen | +--------------------------------------------------------------------------+ */ /** * 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(); $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)); // configure the cache $memcache_hosts = $this->conf->get('kolab_wap', 'memcache_hosts'); $this->config_set("memcache_pconnect", $this->conf->get('kolab_wap', 'memcache_pconnect', Conf::BOOL)); $this->config_set("memcache_hosts", $memcache_hosts); $this->config_set("cache", !empty($memcache_hosts)); $this->config_set("domain_base_dn", $this->conf->get('ldap', 'domain_base_dn')); $this->config_set("domain_filter", $this->conf->get('ldap', 'domain_filter')); $this->config_set("domain_name_attribute", $this->conf->get('ldap', 'domain_name_attribute')); // 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_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_uri); $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, &$attributes = null) { $this->_log(LOG_DEBUG, "Auth::LDAP: authentication request for $username against domain $domain"); if (!$domain) { $domain = $this->domain; } $result = $this->login($username, $password, $domain, $attributes); 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($attributes['domainrelatedobject_only'])) { $domainrelatedobject_only = (bool)($attributes['domainrelatedobject_only']); unset($attributes['domainrelatedobject_only']); } else { $domainrelatedobject_only = false; } 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; if (!empty($attributes['inetdomainbasedn'])) { $inetdomainbasedn = $attributes['inetdomainbasedn']; } else { $inetdomainbasedn = $this->_standard_root_dn($domain); } if (empty($attributes['aci'])) { $attributes['aci'] = array( "(targetattr = \"*\") (version 3.0;acl \"Read Access for {$domain} Users\";allow (read,compare,search)(userdn = \"ldap:///{$inetdomainbasedn}??sub?(objectclass=*)\");)" ); } $result = $this->add_entry($domain_dn, $attributes); if (!$result) { return false; } // Return if the request specified to only create the domainrelatedobject if ($domainrelatedobject_only) { return true; } // Query the ACI for the primary domain if ($domain_entry = $this->find_domain($primary_domain)) { 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; } + // remove invalid/hidden attributes + if (isset($attributes['domainrelatedobject_only'])) { + unset($attributes['domainrelatedobject_only']); + } + $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) { if ($result = $this->find_domain($domain, $attributes)) { $result_dn = $result['dn']; unset($result['dn']); $result = array($result_dn => $result); } } 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|array $domain Domain name or domain_info() result * * @return bool True if domain is empty, False otherwise */ public function domain_is_empty($domain) { $domain_name_attribute = $this->conf->get('ldap', 'domain_name_attribute'); if (empty($domain_name_attribute)) { $domain_name_attribute = 'associateddomain'; } if (!is_array($domain)) { $domain = $this->domain_info($domain); } if (!empty($domain)) { $domain_dn = key($domain); $domain_name = $domain[$domain_dn][$domain_name_attribute]; if (is_array($domain_name)) { $domain_name = $domain_name[0]; } } 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) { $base_dn = $this->entry_base_dn('group', $typeid, $attrs); // TODO: The rdn is configurable as well. // Use [$type_str . "_"]user_rdn_attr $dn = "cn=" . Net_LDAP3::quote_string($attrs['cn'], true) . "," . $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($params['type'] ? $params['type'] . '_ou' : '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) { $base_dn = $this->entry_base_dn('ou', $typeid, $attrs); // TODO: The rdn is configurable as well. // Use [$type_str . "_"]ou_rdn_attr $dn = "ou=" . Net_LDAP3::quote_string($attrs['ou'], true) . "," . $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) { $base_dn = $this->entry_base_dn('resource', $typeid, $attrs); // TODO: The rdn is configurable as well. // Use [$type_str . "_"]resource_rdn_attr $dn = "cn=" . Net_LDAP3::quote_string($attrs['cn'], true) . "," . $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, $attrs); // TODO: The rdn is configurable as well. // Use [$type_str . "_"]role_rdn_attr $dn = "cn=" . Net_LDAP3::quote_string($attrs['cn'], true) . "," . $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, $attrs); // TODO: The rdn is configurable as well. // Use [$type_str . "_"]user_rdn_attr $dn = "cn=" . Net_LDAP3::quote_string($attrs['cn'], true) . "," . $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', $attributes = null, $props = array(), $count_only = false) { 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); } return parent::search($base_dn, $filter, $scope, $attributes ?: array('*'), $props, $count_only); } 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, $attrs); // TODO: The rdn is configurable as well. // Use [$type_str . "_"]user_rdn_attr $dn = "uid=" . Net_LDAP3::quote_string($attrs['uid'], true) . "," . $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; } /** * Find domain by name * * @param string $domain Domain name * @param array $attributes Result attributes * * @return array|bool Domain attributes (+ 'dn' attribute) or False on error */ public function find_domain($domain, $attributes = array('*')) { if (empty($domain)) { return false; } $ckey = 'domain::' . $domain . '::' . md5(implode(',', $attributes)); if (isset($this->icache[$ckey])) { return $this->icache[$ckey]; } // connect and bind... if ($_SESSION['user'] && $_SESSION['user']->user_bind_dn) { $bind_dn = $_SESSION['user']->user_bind_dn; $bind_pw = $_SESSION['user']->user_bind_pw; } else { $bind_dn = $this->conf->get('service_bind_dn'); $bind_pw = $this->conf->get('service_bind_pw'); } if (!$this->bind($bind_dn, $bind_pw)) { return false; } return parent::find_domain($domain, $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('*'); } // LDAP3 search parameters $opts = array( 'search' => $search, 'sort' => $params['sort_by'], // for VLV ); $result = $this->search_entries($base_dn, $filter, $scope, $attributes, $opts); $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) { 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, &$attrs = array()) { // check if base_dn already exists in object attributes if (!empty($attrs)) { if (!empty($attrs['base_dn'])) { $base_dn = $attrs['base_dn']; unset($attrs['base_dn']); } else if ($type != 'ou' && !empty($attrs['ou'])) { $base_dn = $attrs['ou']; unset($attrs['ou']); } } if (empty($base_dn) && $typeid) { $db = SQL::get_instance(); $query = $db->query("SELECT `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 _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"); } // This could be "_", if so we'll try the name only now if (empty($subject_base_dn) && ($pos = strrpos($subject, '_'))) { $subject = substr($subject, $pos + 1); $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); 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 **************** ***********************************************************/ protected 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; } } /** * 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; } }