aboutsummaryrefslogtreecommitdiffstats
path: root/apps/user_ldap/lib/access.php
diff options
context:
space:
mode:
Diffstat (limited to 'apps/user_ldap/lib/access.php')
-rw-r--r--apps/user_ldap/lib/access.php592
1 files changed, 592 insertions, 0 deletions
diff --git a/apps/user_ldap/lib/access.php b/apps/user_ldap/lib/access.php
new file mode 100644
index 00000000000..eef471f1598
--- /dev/null
+++ b/apps/user_ldap/lib/access.php
@@ -0,0 +1,592 @@
+<?php
+
+/**
+ * ownCloud – LDAP Access
+ *
+ * @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/>.
+ *
+ */
+
+namespace OCA\user_ldap\lib;
+
+abstract class Access {
+ protected $connection;
+
+ public function setConnector(Connection &$connection) {
+ $this->connection = $connection;
+ }
+
+ private function checkConnection() {
+ return ($this->connection instanceof Connection);
+ }
+
+ /**
+ * @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
+ */
+ public function readAttribute($dn, $attr) {
+ if(!$this->checkConnection()) {
+ \OCP\Util::writeLog('user_ldap', 'No LDAP Connector assigned, access impossible for readAttribute.', \OCP\Util::WARN);
+ return false;
+ }
+ $cr = $this->connection->getConnectionResource();
+ $rr = ldap_read($cr, $dn, 'objectClass=*', array($attr));
+ $er = ldap_first_entry($cr, $rr);
+ //LDAP attributes are not case sensitive
+ $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[] = $this->resemblesDN($attr) ? $this->sanitizeDN($result[$attr][$i]) : $result[$attr][$i];
+ }
+ return $values;
+ }
+ return false;
+ }
+
+ /**
+ * @brief checks wether the given attribute`s valua is probably a DN
+ * @param $attr the attribute in question
+ * @return if so true, otherwise false
+ */
+ private function resemblesDN($attr) {
+ $resemblingAttributes = array(
+ 'dn',
+ 'uniquemember',
+ 'member'
+ );
+ return in_array($attr, $resemblingAttributes);
+ }
+
+ /**
+ * @brief sanitizes a DN received from the LDAP server
+ * @param $dn the DN in question
+ * @return the sanitized DN
+ */
+ 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!
+ $dn = preg_replace('/([^\\\]),(\s+)/u','\1,',$dn);
+
+ //make comparisons and everything work
+ $dn = mb_strtolower($dn, 'UTF-8');
+
+ return $dn;
+ }
+
+ /**
+ * gives back the database table for the query
+ */
+ 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
+ */
+ public function groupname2dn($name) {
+ return $this->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
+ */
+ public function username2dn($name) {
+ $dn = $this->ocname2dn($name, true);
+ if($dn) {
+ return $dn;
+ } else {
+ //fallback: user is not mapped
+ $filter = $this->combineFilterWithAnd(array(
+ $this->connection->ldapUserFilter,
+ $this->connection->ldapUserDisplayName . '=' . $name,
+ ));
+ $result = $this->searchUsers($filter, 'dn');
+ if(isset($result[0]['dn'])) {
+ $this->mapComponent($result[0], $name, true);
+ return $result[0];
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @brief returns the LDAP DN for the given internal ownCloud name
+ * @param $name the ownCloud name in question
+ * @param $isUser is it a user? otherwise group
+ * @returns string with the LDAP DN on success, otherwise false
+ *
+ * returns the LDAP DN for the given internal ownCloud name
+ */
+ private function ocname2dn($name, $isUser) {
+ $table = $this->getMapTable($isUser);
+
+ $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
+ */
+ public function dn2groupname($dn, $ldapname = null) {
+ if(mb_strripos($dn, $this->connection->ldapBaseGroups, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($this->connection->ldapBaseGroups, 'UTF-8'))) {
+ return false;
+ }
+ return $this->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
+ */
+ public function dn2username($dn, $ldapname = null) {
+ if(mb_strripos($dn, $this->connection->ldapBaseUsers, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($this->connection->ldapBaseUsers, 'UTF-8'))) {
+ return false;
+ }
+ return $this->dn2ocname($dn, $ldapname, true);
+ }
+
+ /**
+ * @brief returns an internal ownCloud name for the given LDAP DN
+ * @param $dn the dn of the user object
+ * @param $ldapname optional, the display name of the object
+ * @param $isUser optional, wether it is a user object (otherwise group assumed)
+ * @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
+ */
+ public function dn2ocname($dn, $ldapname = null, $isUser = true) {
+ $dn = $this->sanitizeDN($dn);
+ $table = $this->getMapTable($isUser);
+ if($isUser) {
+ $nameAttribute = $this->connection->ldapUserDisplayName;
+ } else {
+ $nameAttribute = $this->connection->ldapGroupDisplayName;
+ }
+
+ $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 = $this->readAttribute($dn, $nameAttribute);
+ $ldapname = $ldapname[0];
+ }
+ $ldapname = $this->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($this->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 = $this->alternateOwnCloudName($ldapname, $dn);
+ if($this->mapComponent($dn, $oc_name, $isUser)) {
+ return $oc_name;
+ }
+
+ //TODO: do not simple die away!
+ //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
+ */
+ public function ownCloudUserNames($ldapUsers) {
+ return $this->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
+ */
+ public function ownCloudGroupNames($ldapGroups) {
+ return $this->ldap2ownCloudNames($ldapGroups, false);
+ }
+
+ private function ldap2ownCloudNames($ldapObjects, $isUsers) {
+ if($isUsers) {
+ $knownObjects = $this->mappedUsers();
+ $nameAttribute = $this->connection->ldapUserDisplayName;
+ } else {
+ $knownObjects = $this->mappedGroups();
+ $nameAttribute = $this->connection->ldapGroupDisplayName;
+ }
+ $ownCloudNames = array();
+
+ foreach($ldapObjects as $ldapObject) {
+// if(!isset($ldapObject['dn'])) {
+// print("<pre>");
+// die(var_dump($ldapObjects));
+// }
+ $key = \OCP\Util::recursiveArraySearch($knownObjects, $ldapObject['dn']);
+
+ //everything is fine when we know the group
+ 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 = $this->sanitizeUsername($ldapObject[$nameAttribute]);
+ if($this->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 = $this->alternateOwnCloudName($ocname, $ldapObject['dn']);
+ if($this->mapComponent($ldapObject['dn'], $ocname, $isUsers)) {
+ $ownCloudNames[] = $ocname;
+ continue;
+ }
+
+ //TODO: do not simple die away
+ //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
+ */
+ private function alternateOwnCloudName($name, $dn) {
+ $ufn = ldap_dn2ufn($dn);
+ $name = $name . '@' . trim(\OCP\Util::mb_substr_replace($ufn, '', 0, mb_strpos($ufn, ',', 0, 'UTF-8'), 'UTF-8'));
+ $name = $this->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
+ */
+ private function mappedGroups() {
+ return $this->mappedComponents(false);
+ }
+
+ /**
+ * @brief retrieves all known users from the mappings table
+ * @returns array with the results
+ *
+ * retrieves all known users from the mappings table
+ */
+ private function mappedUsers() {
+ return $this->mappedComponents(true);
+ }
+
+ private function mappedComponents($isUsers) {
+ $table = $this->getMapTable($isUsers);
+
+ $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
+ */
+ private function mapComponent($dn, $ocname, $isUser = true) {
+ $table = $this->getMapTable($isUser);
+ $dn = $this->sanitizeDN($dn);
+
+ $sqlAdjustment = '';
+ $dbtype = \OCP\Config::getSystemValue('dbtype');
+ if($dbtype == 'mysql') {
+ $sqlAdjustment = 'FROM dual';
+ }
+
+ $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;
+ }
+
+ public function fetchListOfUsers($filter, $attr) {
+ return $this->fetchList($this->searchUsers($filter, $attr), (count($attr) > 1));
+ }
+
+ public function fetchListOfGroups($filter, $attr) {
+ return $this->fetchList($this->searchGroups($filter, $attr), (count($attr) > 1));
+ }
+
+ 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 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
+ */
+ public function searchUsers($filter, $attr = null) {
+ return $this->search($filter, $this->connection->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
+ */
+ public function searchGroups($filter, $attr = null) {
+ return $this->search($filter, $this->connection->ldapBaseGroups, $attr);
+ }
+
+ /**
+ * @brief executes an LDAP search
+ * @param $filter the LDAP filter for the search
+ * @param $base the LDAP subtree that shall be searched
+ * @param $attr optional, when a certain attribute shall be filtered out
+ * @returns array with the search result
+ *
+ * Executes an LDAP search
+ */
+ private function search($filter, $base, $attr = null) {
+ if(!is_null($attr) && !is_array($attr)) {
+ $attr = array(mb_strtolower($attr, 'UTF-8'));
+ }
+
+ // See if we have a resource
+ $link_resource = $this->connection->getConnectionResource();
+ if(is_resource($link_resource)) {
+ $sr = ldap_search($link_resource, $base, $filter, $attr);
+ $findings = ldap_get_entries($link_resource, $sr );
+
+ // 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.
+ \OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', \OCP\Util::DEBUG);
+ 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;
+ }
+ $item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
+
+ if($multiarray) {
+ foreach($attr as $key) {
+ $key = mb_strtolower($key, 'UTF-8');
+ if(isset($item[$key])) {
+ if($key != 'dn'){
+ $selection[$i][$key] = $this->resemblesDN($key) ? $this->sanitizeDN($item[$key][0]) : $item[$key][0];
+ } else {
+ $selection[$i][$key] = $this->sanitizeDN($item[$key]);
+ }
+ }
+
+ }
+ $i++;
+ } else {
+ //tribute to case insensitivity
+ $key = mb_strtolower($attr[0], 'UTF-8');
+
+ if(isset($item[$key])) {
+ if($this->resemblesDN($key)) {
+ $selection[] = $this->sanitizeDN($item[$key]);
+ } else {
+ $selection[] = $item[$key];
+ }
+ }
+ }
+ }
+ return $selection;
+ }
+ return $findings;
+ }
+
+ public function sanitizeUsername($name) {
+ if($this->connection->ldapIgnoreNamingRules) {
+ return $name;
+ }
+
+ //REPLACEMENTS
+ $name = \OCP\Util::mb_str_replace(' ', '_', $name, 'UTF-8');
+
+ //every remaining unallowed characters will be removed
+ $name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
+
+ return $name;
+ }
+
+ /**
+ * @brief combines the input filters with AND
+ * @param $filters array, the filters to connect
+ * @returns the combined filter
+ *
+ * Combines Filter arguments with AND
+ */
+ public function combineFilterWithAnd($filters) {
+ return $this->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
+ */
+ public function combineFilterWithOr($filters) {
+ return $this->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
+ */
+ private function combineFilter($filters, $operator) {
+ $combinedFilter = '('.$operator;
+ foreach($filters as $filter) {
+ if($filter[0] != '(') {
+ $filter = '('.$filter.')';
+ }
+ $combinedFilter.=$filter;
+ }
+ $combinedFilter.=')';
+ return $combinedFilter;
+ }
+
+ public function areCredentialsValid($name, $password) {
+ $testConnection = clone $this->connection;
+ $credentials = array(
+ 'ldapAgentName' => $name,
+ 'ldapAgentPassword' => $password
+ );
+ if(!$testConnection->setConfiguration($credentials)) {
+ return false;
+ }
+ return $testConnection->bind();
+ }
+} \ No newline at end of file