diff --git a/lib/Auth/LDAP.php b/lib/Auth/LDAP.php index 3845767..626c131 100644 --- a/lib/Auth/LDAP.php +++ b/lib/Auth/LDAP.php @@ -1,1503 +1,1503 @@ | +--------------------------------------------------------------------------+ | 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(!empty($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'] ?? [], $attrs); } } - $attributes['may'] = array_merge($attributes['may'], $additional_attributes['*']); + $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 (!empty($_SESSION['user']) && !empty($_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, // T409349: Remove the 'sort' parameter to use default VLV sorting // If the param value is not the same as in VLV, search will not use the index. // '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']); + $attributes = $this->attributes_allowed($subject[$subject_dn]['objectclass'] ?? []); + $attributes = array_merge($attributes['may'] ?? [], $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)) { $str1 = $str2 = ''; 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; } } diff --git a/lib/api/kolab_api_service_type.php b/lib/api/kolab_api_service_type.php index 026d1be..6d7b59e 100644 --- a/lib/api/kolab_api_service_type.php +++ b/lib/api/kolab_api_service_type.php @@ -1,261 +1,262 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | | Author: Jeroen van Meeuwen | +--------------------------------------------------------------------------+ */ /** * Service providing object types management */ class kolab_api_service_type extends kolab_api_service { /** * Returns service capabilities. * * @param string $domain Domain name * * @return array Capabilities list */ public function capabilities($domain) { $effective_rights = $this->type_effective_rights(); + $entry_rights = $effective_rights['entrylevelrights'] ?? array(); $rights = array(); - if (in_array('add', (array)$effective_rights['entrylevelrights'])) { + if (in_array('add', $entry_rights)) { $rights['add'] = "w"; } - if (in_array('delete', (array)$effective_rights['entrylevelrights'])) { + if (in_array('delete', $entry_rights)) { $rights['delete'] = "w"; } - if (in_array('modrdn', (array)$effective_rights['entrylevelrights'])) { + if (in_array('modrdn', $entry_rights)) { $rights['edit'] = "w"; } - if (in_array('read', (array)$effective_rights['entrylevelrights'])) { + if (in_array('read', $entry_rights)) { $rights['info'] = "r"; } $rights['effective_rights'] = "r"; return $rights; } /** * Create type. * * @param array $get GET parameters * @param array $post POST parameters * * @return array|bool Type attributes or False on error. */ public function type_add($getdata, $postdata) { if (!in_array($postdata['type'], $this->supported_types_db)) { return false; } if (empty($postdata['name']) || empty($postdata['key'])) { return false; } if (empty($postdata['attributes']) || !is_array($postdata['attributes'])) { return false; } $effective_rights = $this->type_effective_rights(); if (!in_array('add', (array)$effective_rights['entrylevelrights'])) { return false; } $type = $postdata['type']; $query = array( 'key' => $postdata['key'], 'name' => $postdata['name'], 'description' => $postdata['description'] ? $postdata['description'] : '', 'attributes' => json_encode($postdata['attributes']), 'is_default' => !empty($postdata['is_default']) ? 1 : 0, ); if ($postdata['type'] == 'user') { $query['used_for'] = $postdata['used_for'] == 'hosted' ? 'hosted' : null; } $query = array_map(array($this->db, 'quote'), $query); $columns = array_map(array($this->db, 'quote_identifier'), array_keys($query)); $this->db->query("INSERT INTO `{$type}_types`" . " (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $query) . ")"); if (!($id = $this->db->insert_id($type . '_types'))) { return false; } // there can be only one default if (!empty($postdata['is_default'])) { $this->db->query("UPDATE `{$type}_types` SET `is_default` = 0 WHERE `id` <> " . intval($id)); } $postdata['id'] = $id; return $postdata; } /** * Detete type. * * @param array $get GET parameters * @param array $post POST parameters * * @return bool True on success, False on failure */ public function type_delete($getdata, $postdata) { if (empty($postdata['type']) || empty($postdata['id'])) { return false; } if (!in_array($postdata['type'], $this->supported_types_db)) { return false; } $object_name = $postdata['type']; $object_id = $postdata['id']; $effective_rights = $this->type_effective_rights(); if (!in_array('delete', (array)$effective_rights['entrylevelrights'])) { return false; } $this->db->query("DELETE FROM `{$object_name}_types` WHERE `id` = " . intval($object_id)); return (bool) $this->db->affected_rows(); } /** * Update type. * * @param array $get GET parameters * @param array $post POST parameters * * @return bool True on success, False on failure */ public function type_edit($getdata, $postdata) { if (empty($postdata['type']) || empty($postdata['id'])) { return false; } if (empty($postdata['name']) || empty($postdata['key'])) { return false; } if (empty($postdata['attributes']) || !is_array($postdata['attributes'])) { return false; } $effective_rights = $this->type_effective_rights(); if (!in_array('modrdn', (array)$effective_rights['entrylevelrights'])) { return false; } $type = $postdata['type']; $query = array( 'key' => $postdata['key'], 'name' => $postdata['name'], 'description' => !empty($postdata['description']) ? $postdata['description'] : '', 'attributes' => json_encode($postdata['attributes']), 'is_default' => !empty($postdata['is_default']) ? 1 : 0, ); if ($postdata['type'] == 'user') { $query['used_for'] = !empty($postdata['used_for']) && $postdata['used_for'] == 'hosted' ? 'hosted' : null; } foreach ($query as $idx => $value) { $query[$idx] = $this->db->quote_identifier($idx) . " = " . $this->db->quote($value); } $result = $this->db->query("UPDATE `{$type}_types` SET " . implode(', ', $query) . " WHERE `id` = " . intval($postdata['id'])); if (!$result) { return false; } // there can be only one default if (!empty($postdata['is_default'])) { $this->db->query("UPDATE `{$type}_types` SET `is_default` = 0 WHERE `id` <> " . intval($postdata['id'])); } return $postdata; } public function type_effective_rights($getdata = null, $postdata = null) { $effective_rights = array(); // @TODO: set rights according to user group or sth if (strtolower($_SESSION['user']->get_userid()) == 'cn=directory manager') { $attr_acl = array('read', 'write', 'delete'); $effective_rights = array( 'entrylevelrights' => array( 'read', 'add', 'delete', 'modrdn', ), 'attributelevelrights' => array( 'key' => $attr_acl, 'name' => $attr_acl, 'description' => $attr_acl, 'used_for' => $attr_acl, 'attributes' => $attr_acl, ), ); } return $effective_rights; } /** * Type information. * * @param array $get GET parameters * @param array $post POST parameters * * @return array|bool Type data, False on error */ public function type_info($getdata, $postdata) { if (empty($getdata['type']) || empty($getdata['id'])) { return false; } if (!in_array($getdata['type'], $this->supported_types_db)) { return false; } $object_name = $getdata['type']; $object_id = $getdata['id']; $types = $this->object_types($object_name); return !empty($types[$object_id]) ? $types[$object_id] : false; } }