nextcloud/apps/user_ldap/lib_ldap.php

722 lines
22 KiB
PHP
Raw Normal View History

<?php
/**
* ownCloud LDAP lib
*
* @author Arthur Schiwon
* @copyright 2012 Arthur Schiwon blizzz@owncloud.com
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
define('LDAP_GROUP_MEMBER_ASSOC_ATTR','uniqueMember');
define('LDAP_GROUP_DISPLAY_NAME_ATTR','cn');
2012-04-15 15:37:35 +04:00
2012-04-16 20:03:02 +04:00
//needed to unbind, because we use OC_LDAP only statically
class OC_LDAP_DESTRUCTOR {
public function __destruct() {
OC_LDAP::destruct();
}
}
2012-04-15 15:37:35 +04:00
class OC_LDAP {
static protected $ldapConnectionRes = false;
static protected $configured = false;
//cached settings
static protected $ldapHost;
static protected $ldapPort;
static protected $ldapBase;
static protected $ldapBaseUsers;
static protected $ldapBaseGroups;
static protected $ldapAgentName;
static protected $ldapAgentPassword;
static protected $ldapTLS;
static protected $ldapNoCase;
static protected $ldapIgnoreNamingRules;
// user and group settings, that are needed in both backends
static protected $ldapUserDisplayName;
static protected $ldapUserFilter;
static protected $ldapGroupDisplayName;
static protected $ldapLoginFilter;
2012-06-07 20:13:41 +04:00
static protected $__d;
/**
* @brief initializes the LDAP backend
* @param $force read the config settings no matter what
*
* initializes the LDAP backend
*/
static public function init($force = false) {
2012-06-07 20:13:41 +04:00
if(is_null(self::$__d)) {
self::$__d = new OC_LDAP_DESTRUCTOR();
}
self::readConfiguration($force);
self::establishConnection();
}
2012-04-16 20:03:02 +04:00
static public function destruct() {
@ldap_unbind(self::$ldapConnectionRes);
}
/**
* @brief returns a read-only configuration value
* @param $key the name of the configuration value
* @returns the value on success, otherwise null
*
* returns a read-only configuration values
*
* we cannot work with getters, because it is a static class
*/
static public function conf($key) {
if(!self::$configured) {
self::init();
}
2012-04-16 15:27:20 +04:00
$availableProperties = array(
'ldapUserDisplayName',
'ldapGroupDisplayName',
'ldapLoginFilter'
2012-04-16 15:27:20 +04:00
);
if(in_array($key, $availableProperties)) {
return self::$$key;
}
return null;
}
/**
* gives back the database table for the query
*/
static private function getMapTable($isUser) {
if($isUser) {
return '*PREFIX*ldap_user_mapping';
} else {
return '*PREFIX*ldap_group_mapping';
}
}
/**
* @brief returns the LDAP DN for the given internal ownCloud name of the group
* @param $name the ownCloud name in question
* @returns string with the LDAP DN on success, otherwise false
*
* returns the LDAP DN for the given internal ownCloud name of the group
*/
static public function groupname2dn($name) {
return self::ocname2dn($name, false);
}
/**
* @brief returns the LDAP DN for the given internal ownCloud name of the user
* @param $name the ownCloud name in question
* @returns string with the LDAP DN on success, otherwise false
*
* returns the LDAP DN for the given internal ownCloud name of the user
*/
static public function username2dn($name) {
$dn = self::ocname2dn($name, true);
if($dn) {
return $dn;
} else {
//fallback: user is not mapped
self::init();
$filter = self::combineFilterWithAnd(array(
self::$ldapUserFilter,
self::$ldapUserDisplayName . '=' . $name,
));
$result = self::searchUsers($filter, 'dn');
if(isset($result[0]['dn'])) {
self::mapUser($result[0], $name);
return $result[0];
}
}
return false;
}
static private function ocname2dn($name, $isUser) {
$table = self::getMapTable($isUser);
2012-05-03 15:06:08 +04:00
$query = OCP\DB::prepare('
SELECT ldap_dn
FROM '.$table.'
WHERE owncloud_name = ?
');
$record = $query->execute(array($name))->fetchOne();
return $record;
}
/**
* @brief returns the internal ownCloud name for the given LDAP DN of the group
* @param $dn the dn of the group object
* @param $ldapname optional, the display name of the object
* @returns string with with the name to use in ownCloud, false on DN outside of search DN
*
* returns the internal ownCloud name for the given LDAP DN of the group
*/
static public function dn2groupname($dn, $ldapname = null) {
2012-07-02 22:31:07 +04:00
if(mb_strripos($dn, self::$ldapBaseGroups, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen(self::$ldapBaseGroups, 'UTF-8'))) {
return false;
}
return self::dn2ocname($dn, $ldapname, false);
}
/**
* @brief returns the internal ownCloud name for the given LDAP DN of the user
* @param $dn the dn of the user object
* @param $ldapname optional, the display name of the object
* @returns string with with the name to use in ownCloud
*
* returns the internal ownCloud name for the given LDAP DN of the user, false on DN outside of search DN
*/
static public function dn2username($dn, $ldapname = null) {
2012-07-02 22:31:07 +04:00
if(mb_strripos($dn, self::$ldapBaseUsers, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen(self::$ldapBaseUsers, 'UTF-8'))) {
return false;
}
return self::dn2ocname($dn, $ldapname, true);
}
static public function dn2ocname($dn, $ldapname = null, $isUser = true) {
$dn = self::sanitizeDN($dn);
$table = self::getMapTable($isUser);
if($isUser) {
$nameAttribute = self::conf('ldapUserDisplayName');
} else {
$nameAttribute = self::conf('ldapGroupDisplayName');
}
2012-05-03 15:06:08 +04:00
$query = OCP\DB::prepare('
SELECT owncloud_name
FROM '.$table.'
WHERE ldap_dn = ?
');
$component = $query->execute(array($dn))->fetchOne();
if($component) {
return $component;
}
if(is_null($ldapname)) {
$ldapname = self::readAttribute($dn, $nameAttribute);
$ldapname = $ldapname[0];
}
$ldapname = self::sanitizeUsername($ldapname);
//a new user/group! Then let's try to add it. We're shooting into the blue with the user/group name, assuming that in most cases there will not be a conflict. Otherwise an error will occur and we will continue with our second shot.
if(self::mapComponent($dn, $ldapname, $isUser)) {
return $ldapname;
}
//doh! There is a conflict. We need to distinguish between users/groups. Adding indexes is an idea, but not much of a help for the user. The DN is ugly, but for now the only reasonable way. But we transform it to a readable format and remove the first part to only give the path where this object is located.
$oc_name = self::alternateOwnCloudName($ldapname, $dn);
if(self::mapComponent($dn, $oc_name, $isUser)) {
return $oc_name;
}
//and this of course should never been thrown :)
throw new Exception('LDAP backend: unexpected collision of DN and ownCloud Name.');
}
/**
* @brief gives back the user names as they are used ownClod internally
* @param $ldapGroups an array with the ldap Users result in style of array ( array ('dn' => foo, 'uid' => bar), ... )
* @returns an array with the user names to use in ownCloud
*
* gives back the user names as they are used ownClod internally
*/
static public function ownCloudUserNames($ldapUsers) {
return self::ldap2ownCloudNames($ldapUsers, true);
}
/**
* @brief gives back the group names as they are used ownClod internally
* @param $ldapGroups an array with the ldap Groups result in style of array ( array ('dn' => foo, 'cn' => bar), ... )
* @returns an array with the group names to use in ownCloud
*
* gives back the group names as they are used ownClod internally
*/
static public function ownCloudGroupNames($ldapGroups) {
return self::ldap2ownCloudNames($ldapGroups, false);
}
static private function ldap2ownCloudNames($ldapObjects, $isUsers) {
if($isUsers) {
$knownObjects = self::mappedUsers();
$nameAttribute = self::conf('ldapUserDisplayName');
} else {
$knownObjects = self::mappedGroups();
$nameAttribute = self::conf('ldapGroupDisplayName');
}
$ownCloudNames = array();
foreach($ldapObjects as $ldapObject) {
$key = self::recursiveArraySearch($knownObjects, $ldapObject['dn']);
//everything is fine when we know the group
2012-06-07 14:44:59 +04:00
if($key !== false) {
$ownCloudNames[] = $knownObjects[$key]['owncloud_name'];
continue;
}
//a new group! Then let's try to add it. We're shooting into the blue with the group name, assuming that in most cases there will not be a conflict. But first make sure, that the display name contains only allowed characters.
$ocname = self::sanitizeUsername($ldapObject[$nameAttribute]);
if(self::mapComponent($ldapObject['dn'], $ocname, $isUsers)) {
$ownCloudNames[] = $ocname;
continue;
}
//doh! There is a conflict. We need to distinguish between groups. Adding indexes is an idea, but not much of a help for the user. The DN is ugly, but for now the only reasonable way. But we transform it to a readable format and remove the first part to only give the path where this entry is located.
$ocname = self::alternateOwnCloudName($ocname, $ldapObject['dn']);
if(self::mapComponent($ldapObject['dn'], $ocname, $isUsers)) {
$ownCloudNames[] = $ocname;
continue;
}
//and this of course should never been thrown :)
throw new Exception('LDAP backend: unexpected collision of DN and ownCloud Name.');
}
return $ownCloudNames;
}
/**
* @brief creates a hopefully unique name for owncloud based on the display name and the dn of the LDAP object
* @param $name the display name of the object
* @param $dn the dn of the object
* @returns string with with the name to use in ownCloud
*
* creates a hopefully unique name for owncloud based on the display name and the dn of the LDAP object
*/
static private function alternateOwnCloudName($name, $dn) {
$ufn = ldap_dn2ufn($dn);
2012-07-02 22:31:07 +04:00
$name = $name . '@' . trim(OCP\Util::mb_substr_replace($ufn, '', 0, mb_strpos($ufn, ',', 0, 'UTF-8'), 'UTF-8'));
$name = self::sanitizeUsername($name);
return $name;
}
/**
* @brief retrieves all known groups from the mappings table
* @returns array with the results
*
* retrieves all known groups from the mappings table
*/
static private function mappedGroups() {
return self::mappedComponents(false);
}
/**
* @brief retrieves all known users from the mappings table
* @returns array with the results
*
* retrieves all known users from the mappings table
*/
static private function mappedUsers() {
return self::mappedComponents(true);
}
static private function mappedComponents($isUsers) {
$table = self::getMapTable($isUsers);
2012-05-03 15:06:08 +04:00
$query = OCP\DB::prepare('
SELECT ldap_dn, owncloud_name
FROM '. $table
);
return $query->execute()->fetchAll();
}
/**
* @brief inserts a new user or group into the mappings table
* @param $dn the record in question
* @param $ocname the name to use in ownCloud
* @param $isUser is it a user or a group?
* @returns true on success, false otherwise
*
* inserts a new user or group into the mappings table
*/
static private function mapComponent($dn, $ocname, $isUser = true) {
$table = self::getMapTable($isUser);
$dn = self::sanitizeDN($dn);
$sqlAdjustment = '';
2012-05-08 15:56:07 +04:00
$dbtype = OCP\Config::getSystemValue('dbtype');
if($dbtype == 'mysql') {
$sqlAdjustment = 'FROM dual';
2012-05-08 15:56:07 +04:00
}
2012-05-03 15:06:08 +04:00
$insert = OCP\DB::prepare('
INSERT INTO '.$table.' (ldap_dn, owncloud_name)
SELECT ?,?
'.$sqlAdjustment.'
WHERE NOT EXISTS (
SELECT 1
FROM '.$table.'
WHERE ldap_dn = ?
OR owncloud_name = ? )
');
$res = $insert->execute(array($dn, $ocname, $dn, $ocname));
if(OCP\DB::isError($res)) {
return false;
}
$insRows = $res->numRows();
if($insRows == 0) {
return false;
}
return true;
}
static public function fetchListOfUsers($filter, $attr) {
return self::fetchList(OC_LDAP::searchUsers($filter, $attr), (count($attr) > 1));
}
static public function fetchListOfGroups($filter, $attr) {
return self::fetchList(OC_LDAP::searchGroups($filter, $attr), (count($attr) > 1));
}
static private function fetchList($list, $manyAttributes) {
if(is_array($list)) {
if($manyAttributes) {
return $list;
} else {
return array_unique($list, SORT_LOCALE_STRING);
}
}
//error cause actually, maybe throw an exception in future.
return array();
}
/**
* @brief reads a given attribute for an LDAP record identified by a DN
* @param $dn the record in question
* @param $attr the attribute that shall be retrieved
* @returns the values in an array on success, false otherwise
*
* Reads an attribute from an LDAP entry
*/
static public function readAttribute($dn, $attr) {
$cr = self::getConnectionResource();
$rr = ldap_read($cr, $dn, 'objectClass=*', array($attr));
$er = ldap_first_entry($cr, $rr);
//LDAP attributes are not case sensitive
2012-07-02 22:31:07 +04:00
$result = OCP\Util::mb_array_change_key_case(ldap_get_attributes($cr, $er), MB_CASE_LOWER, 'UTF-8');
$attr = mb_strtolower($attr, 'UTF-8');
if(isset($result[$attr]) && $result[$attr]['count'] > 0){
$values = array();
for($i=0;$i<$result[$attr]['count'];$i++) {
$values[] = self::resemblesDN($attr) ? self::sanitizeDN($result[$attr][$i]) : $result[$attr][$i];
}
return $values;
}
return false;
}
/**
* @brief executes an LDAP search, optimized for Users
* @param $filter the LDAP filter for the search
* @param $attr optional, when a certain attribute shall be filtered out
* @returns array with the search result
*
* Executes an LDAP search
*/
static public function searchUsers($filter, $attr = null) {
self::init();
return self::search($filter, self::$ldapBaseUsers, $attr);
}
/**
* @brief executes an LDAP search, optimized for Groups
* @param $filter the LDAP filter for the search
* @param $attr optional, when a certain attribute shall be filtered out
* @returns array with the search result
*
* Executes an LDAP search
*/
static public function searchGroups($filter, $attr = null) {
self::init();
return self::search($filter, self::$ldapBaseGroups, $attr);
}
2012-04-14 20:26:46 +04:00
/**
* @brief executes an LDAP search
* @param $filter the LDAP filter for the search
* @param $base the LDAP subtree that shall be searched
2012-04-14 20:26:46 +04:00
* @param $attr optional, when a certain attribute shall be filtered out
* @returns array with the search result
*
* Executes an LDAP search
*/
static private function search($filter, $base, $attr = null) {
if(!is_null($attr) && !is_array($attr)) {
2012-07-02 22:31:07 +04:00
$attr = array(mb_strtolower($attr, 'UTF-8'));
}
2012-06-23 00:57:35 +04:00
// See if we have a resource
$link_resource = self::getConnectionResource();
if(is_resource($link_resource)) {
$sr = ldap_search($link_resource, $base, $filter, $attr);
$findings = ldap_get_entries($link_resource, $sr );
2012-06-23 00:57:35 +04:00
// if we're here, probably no connection resource is returned.
// to make ownCloud behave nicely, we simply give back an empty array.
if(is_null($findings)) {
return array();
}
} else {
// Seems like we didn't find any resource.
// Return an empty array just like before.
return array();
}
if(!is_null($attr)) {
$selection = array();
$multiarray = false;
if(count($attr) > 1) {
$multiarray = true;
$i = 0;
}
foreach($findings as $item) {
if(!is_array($item)) {
continue;
}
2012-07-02 22:31:07 +04:00
$item = OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
if($multiarray) {
foreach($attr as $key) {
2012-07-02 22:31:07 +04:00
$key = mb_strtolower($key, 'UTF-8');
if(isset($item[$key])) {
if($key != 'dn'){
$selection[$i][$key] = self::resemblesDN($key) ? self::sanitizeDN($item[$key][0]) : $item[$key][0];
} else {
$selection[$i][$key] = self::sanitizeDN($item[$key]);
}
}
}
$i++;
} else {
//tribute to case insensitivity
2012-07-02 22:31:07 +04:00
$key = mb_strtolower($attr[0], 'UTF-8');
if(isset($item[$key])) {
if(self::resemblesDN($key)) {
$selection[] = self::sanitizeDN($item[$key]);
} else {
$selection[] = $item[$key];
}
}
}
}
return $selection;
}
return $findings;
}
static private function resemblesDN($attr) {
$resemblingAttributes = array(
'dn',
'uniquemember',
'member'
);
return in_array($attr, $resemblingAttributes);
}
static private function sanitizeDN($dn) {
//OID sometimes gives back DNs with whitespace after the comma a la "uid=foo, cn=bar, dn=..." We need to tackle this!
2012-07-02 22:31:07 +04:00
$dn = preg_replace('/([^\\\]),(\s+)/u','\1,',$dn);
//make comparisons and everything work
2012-07-02 22:31:07 +04:00
$dn = mb_strtolower($dn, 'UTF-8');
return $dn;
}
static private function sanitizeUsername($name) {
if(self::$ldapIgnoreNamingRules) {
return $name;
}
//REPLACEMENTS
2012-07-02 22:31:07 +04:00
$name = OCP\Util::mb_str_replace(' ', '_', $name, 'UTF-8');
//every remaining unallowed characters will be removed
2012-07-02 22:31:07 +04:00
$name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
return $name;
}
2012-04-15 15:37:35 +04:00
/**
* @brief combines the input filters with AND
* @param $filters array, the filters to connect
* @returns the combined filter
*
* Combines Filter arguments with AND
*/
static public function combineFilterWithAnd($filters) {
return self::combineFilter($filters,'&');
}
/**
* @brief combines the input filters with AND
* @param $filters array, the filters to connect
* @returns the combined filter
*
* Combines Filter arguments with AND
*/
static public function combineFilterWithOr($filters) {
return self::combineFilter($filters,'|');
}
/**
* @brief combines the input filters with given operator
* @param $filters array, the filters to connect
* @param $operator either & or |
* @returns the combined filter
*
* Combines Filter arguments with AND
*/
static private function combineFilter($filters, $operator) {
$combinedFilter = '('.$operator;
foreach($filters as $filter) {
if($filter[0] != '(') {
2012-04-15 15:37:35 +04:00
$filter = '('.$filter.')';
}
$combinedFilter.=$filter;
}
$combinedFilter.=')';
return $combinedFilter;
}
2012-04-14 20:26:46 +04:00
/**
* Returns the LDAP handler
*/
static private function getConnectionResource() {
if(!self::$ldapConnectionRes) {
self::init();
}
if(is_null(self::$ldapConnectionRes)) {
OCP\Util::writeLog('ldap', 'Connection could not be established', OCP\Util::INFO);
}
return self::$ldapConnectionRes;
}
2012-04-14 20:26:46 +04:00
/**
* Caches the general LDAP configuration.
*/
static private function readConfiguration($force = false) {
if(!self::$configured || $force) {
self::$ldapHost = OCP\Config::getAppValue('user_ldap', 'ldap_host', '');
self::$ldapPort = OCP\Config::getAppValue('user_ldap', 'ldap_port', 389);
self::$ldapAgentName = OCP\Config::getAppValue('user_ldap', 'ldap_dn','');
self::$ldapAgentPassword = base64_decode(OCP\Config::getAppValue('user_ldap', 'ldap_agent_password',''));
self::$ldapBase = OCP\Config::getAppValue('user_ldap', 'ldap_base', '');
self::$ldapBaseUsers = OCP\Config::getAppValue('user_ldap', 'ldap_base_users',self::$ldapBase);
self::$ldapBaseGroups = OCP\Config::getAppValue('user_ldap', 'ldap_base_groups', self::$ldapBase);
self::$ldapTLS = OCP\Config::getAppValue('user_ldap', 'ldap_tls',0);
self::$ldapNoCase = OCP\Config::getAppValue('user_ldap', 'ldap_nocase', 0);
2012-07-02 22:31:07 +04:00
self::$ldapUserDisplayName = mb_strtolower(OCP\Config::getAppValue('user_ldap', 'ldap_display_name', 'uid'), 'UTF-8');
self::$ldapUserFilter = OCP\Config::getAppValue('user_ldap', 'ldap_userlist_filter','objectClass=person');
self::$ldapLoginFilter = OCP\Config::getAppValue('user_ldap', 'ldap_login_filter', '(uid=%uid)');
2012-07-02 22:31:07 +04:00
self::$ldapGroupDisplayName = mb_strtolower(OCP\Config::getAppValue('user_ldap', 'ldap_group_display_name', LDAP_GROUP_DISPLAY_NAME_ATTR), 'UTF-8');
self::$ldapIgnoreNamingRules = OCP\Config::getSystemValue('ldapIgnoreNamingRules', false);
if(empty(self::$ldapBaseUsers)) {
OCP\Util::writeLog('ldap', 'Base for Users is empty, using Base DN', OCP\Util::INFO);
self::$ldapBaseUsers = self::$ldapBase;
}
if(empty(self::$ldapBaseGroups)) {
OCP\Util::writeLog('ldap', 'Base for Groups is empty, using Base DN', OCP\Util::INFO);
self::$ldapBaseGroups = self::$ldapBase;
}
2012-04-16 19:28:58 +04:00
if(
!empty(self::$ldapHost)
&& !empty(self::$ldapPort)
&& (
(!empty(self::$ldapAgentName) && !empty(self::$ldapAgentPassword))
|| ( empty(self::$ldapAgentName) && empty(self::$ldapAgentPassword))
)
&& !empty(self::$ldapBase)
&& !empty(self::$ldapUserDisplayName)
)
{
self::$configured = true;
}
}
}
2012-04-14 20:26:46 +04:00
/**
* Connects and Binds to LDAP
*/
static private function establishConnection() {
2012-04-16 19:28:58 +04:00
if(!self::$configured) {
OCP\Util::writeLog('ldap', 'Configuration is invalid, cannot connect', OCP\Util::INFO);
2012-04-16 19:28:58 +04:00
return false;
}
if(!self::$ldapConnectionRes) {
self::$ldapConnectionRes = ldap_connect(self::$ldapHost, self::$ldapPort);
if(ldap_set_option(self::$ldapConnectionRes, LDAP_OPT_PROTOCOL_VERSION, 3)) {
if(ldap_set_option(self::$ldapConnectionRes, LDAP_OPT_REFERRALS, 0)) {
if(self::$ldapTLS) {
ldap_start_tls(self::$ldapConnectionRes);
}
}
}
$ldapLogin = @ldap_bind(self::$ldapConnectionRes, self::$ldapAgentName, self::$ldapAgentPassword );
if(!$ldapLogin) {
2012-05-08 14:04:52 +04:00
OCP\Util::writeLog('ldap', 'Bind failed: ' . ldap_errno(self::$ldapConnectionRes) . ': ' . ldap_error(self::$ldapConnectionRes), OCP\Util::ERROR);
self::$ldapConnectionRes = null;
return false;
}
}
}
static public function areCredentialsValid($name, $password) {
return @ldap_bind(self::getConnectionResource(), $name, $password);
}
/**
* taken from http://www.php.net/manual/en/function.array-search.php#97645
* TODO: move somewhere, where its better placed since it is not LDAP specific. OC_Helper maybe?
*/
static public function recursiveArraySearch($haystack, $needle, $index = null) {
$aIt = new RecursiveArrayIterator($haystack);
$it = new RecursiveIteratorIterator($aIt);
while($it->valid()) {
if (((isset($index) AND ($it->key() == $index)) OR (!isset($index))) AND ($it->current() == $needle)) {
return $aIt->key();
}
$it->next();
}
return false;
}
}