diff --git a/apps/user_ldap/group_ldap.php b/apps/user_ldap/group_ldap.php index ee3191bec954..5847ce64859c 100644 --- a/apps/user_ldap/group_ldap.php +++ b/apps/user_ldap/group_ldap.php @@ -207,7 +207,7 @@ private function _getGroupDNsFromMemberOf($DN, &$seen = null) { $subGroups = $this->_getGroupDNsFromMemberOf($group, $seen); $allGroups = array_merge($allGroups, $subGroups); } - } + } return $allGroups; } @@ -240,7 +240,7 @@ public function primaryGroupID2Name($gid, $dn) { if(empty($result)) { return false; } - $dn = $result[0]; + $dn = $result[0]['dn'][0]; //and now the group name //NOTE once we have separate ownCloud group IDs and group names we can @@ -484,7 +484,7 @@ private function getGroupsByMember($dn, &$seen = null) { array($this->access->connection->ldapGroupDisplayName, 'dn')); if (is_array($groups)) { foreach ($groups as $groupobj) { - $groupDN = $groupobj['dn']; + $groupDN = $groupobj['dn'][0]; $allGroups[$groupDN] = $groupobj; $nestedGroups = $this->access->connection->ldapNestedGroups; if (!empty($nestedGroups)) { @@ -648,7 +648,7 @@ public function countUsersInGroup($gid, $search = '') { $this->access->connection->ldapLoginFilter, 'UTF-8'), $this->access->getFilterPartForUserSearch($search) )); - $ldap_users = $this->access->fetchListOfUsers($filter, 'dn'); + $ldap_users = $this->access->fetchListOfUsers($filter, 'dn', 1); if(count($ldap_users) < 1) { continue; } diff --git a/apps/user_ldap/lib/access.php b/apps/user_ldap/lib/access.php index 44237b523938..483ca232277e 100644 --- a/apps/user_ldap/lib/access.php +++ b/apps/user_ldap/lib/access.php @@ -486,7 +486,7 @@ public function dn2ocname($fdn, $ldapName = null, $isUser = true) { /** * gives back the user names as they are used ownClod internally - * @param array $ldapUsers an array with the ldap Users result in style of array ( array ('dn' => foo, 'uid' => bar), ... ) + * @param array $ldapUsers as returned by fetchList() * @return array an array with the user names to use in ownCloud * * gives back the user names as they are used ownClod internally @@ -497,7 +497,7 @@ public function ownCloudUserNames($ldapUsers) { /** * gives back the group names as they are used ownClod internally - * @param array $ldapGroups an array with the ldap Groups result in style of array ( array ('dn' => foo, 'cn' => bar), ... ) + * @param array $ldapGroups as returned by fetchList() * @return array an array with the group names to use in ownCloud * * gives back the group names as they are used ownClod internally @@ -507,7 +507,7 @@ public function ownCloudGroupNames($ldapGroups) { } /** - * @param array $ldapObjects + * @param array $ldapObjects as returned by fetchList() * @param bool $isUsers * @return array */ @@ -520,15 +520,25 @@ private function ldap2ownCloudNames($ldapObjects, $isUsers) { $ownCloudNames = array(); foreach($ldapObjects as $ldapObject) { - $nameByLDAP = isset($ldapObject[$nameAttribute]) ? $ldapObject[$nameAttribute] : null; - $ocName = $this->dn2ocname($ldapObject['dn'], $nameByLDAP, $isUsers); + $nameByLDAP = null; + if( isset($ldapObject[$nameAttribute]) + && is_array($ldapObject[$nameAttribute]) + && isset($ldapObject[$nameAttribute][0]) + ) { + // might be set, but not necessarily. if so, we use it. + $nameByLDAP = $ldapObject[$nameAttribute][0]; + } + + $ocName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers); if($ocName) { $ownCloudNames[] = $ocName; if($isUsers) { //cache the user names so it does not need to be retrieved //again later (e.g. sharing dialogue). $this->cacheUserExists($ocName); - $this->cacheUserDisplayName($ocName, $nameByLDAP); + if(!is_null($nameByLDAP)) { + $this->cacheUserDisplayName($ocName, $nameByLDAP); + } } } continue; @@ -536,6 +546,16 @@ private function ldap2ownCloudNames($ldapObjects, $isUsers) { return $ownCloudNames; } + /** + * caches the user display name + * @param string $ocName the internal ownCloud username + * @param string|false $home the home directory path + */ + public function cacheUserHome($ocName, $home) { + $cacheKey = 'getHome'.$ocName; + $this->connection->writeToCache($cacheKey, $home); + } + /** * caches a user as existing * @param string $ocName the internal ownCloud username @@ -657,7 +677,36 @@ public function fetchUsersByLoginName($loginName, $attributes = array('dn')) { * @return array */ public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null) { - return $this->fetchList($this->searchUsers($filter, $attr, $limit, $offset), (count($attr) > 1)); + $ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset); + $this->batchApplyUserAttributes($ldapRecords); + return $this->fetchList($ldapRecords, (count($attr) > 1)); + } + + /** + * provided with an array of LDAP user records the method will fetch the + * user object and requests it to process the freshly fetched attributes and + * and their values + * @param array $ldapRecords + */ + public function batchApplyUserAttributes(array $ldapRecords){ + $displayNameAttribute = strtolower($this->connection->ldapUserDisplayName); + foreach($ldapRecords as $userRecord) { + if(!isset($userRecord[$displayNameAttribute])) { + // displayName is obligatory + continue; + } + + $ocName = $this->dn2ocname($userRecord['dn'][0], $userRecord[$displayNameAttribute]); + if(!$ocName) { + // no user name, skip. + continue; + } + $this->cacheUserExists($ocName); + $user = $this->userManager->get($ocName); + if(!is_null($user)) { + $user->processAttributes($userRecord); + } + } } /** @@ -681,6 +730,11 @@ private function fetchList($list, $manyAttributes) { if($manyAttributes) { return $list; } else { + $list = array_reduce($list, function($carry, $item) { + $attribute = array_keys($item)[0]; + $carry[] = $item[$attribute][0]; + return $carry; + }, array()); return array_unique($list, SORT_LOCALE_STRING); } } @@ -953,44 +1007,29 @@ private function search($filter, $base, $attr = null, $limit = null, $offset = n if(!is_null($attr)) { $selection = array(); - $multiArray = false; - if(count($attr) > 1) { - $multiArray = true; - $i = 0; - } + $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'); - + foreach($attr as $key) { + $key = mb_strtolower($key, 'UTF-8'); if(isset($item[$key])) { - if($this->resemblesDN($key)) { - $selection[] = $this->sanitizeDN($item[$key]); + if(is_array($item[$key]) && isset($item[$key]['count'])) { + unset($item[$key]['count']); + } + if($key !== 'dn') { + $selection[$i][$key] = $this->resemblesDN($key) ? + $this->sanitizeDN($item[$key]) + : $item[$key]; } else { - $selection[] = $item[$key]; + $selection[$i][$key] = [$this->sanitizeDN($item[$key])]; } } + } + $i++; } $findings = $selection; } diff --git a/apps/user_ldap/lib/user/manager.php b/apps/user_ldap/lib/user/manager.php index b70e057741f7..86f0b8991d7f 100644 --- a/apps/user_ldap/lib/user/manager.php +++ b/apps/user_ldap/lib/user/manager.php @@ -126,6 +126,43 @@ private function checkAccess() { } } + /** + * returns a list of attributes that will be processed further, e.g. quota, + * email, displayname, or others. + * @param bool $minimal - optional, set to true to skip attributes with big + * payload + * @return string[] + */ + public function getAttributes($minimal = false) { + $attributes = array('dn', 'uid', 'samaccountname', 'memberof'); + $possible = array( + $this->access->getConnection()->ldapQuotaAttribute, + $this->access->getConnection()->ldapEmailAttribute, + $this->access->getConnection()->ldapUserDisplayName, + ); + foreach($possible as $attr) { + if(!is_null($attr)) { + $attributes[] = $attr; + } + } + + $homeRule = $this->access->getConnection()->homeFolderNamingRule; + if(strpos($homeRule, 'attr:') === 0) { + $attributes[] = substr($homeRule, strlen('attr:')); + } + + if(!$minimal) { + // attributes that are not really important but may come with big + // payload. + $attributes = array_merge($attributes, array( + 'jpegphoto', + 'thumbnailphoto' + )); + } + + return $attributes; + } + /** * Checks whether the specified user is marked as deleted * @param string $id the ownCloud user name diff --git a/apps/user_ldap/lib/user/user.php b/apps/user_ldap/lib/user/user.php index ac5d8f5a374c..637b95db8b41 100644 --- a/apps/user_ldap/lib/user/user.php +++ b/apps/user_ldap/lib/user/user.php @@ -138,6 +138,72 @@ public function update() { } } + /** + * processes results from LDAP for attributes as returned by getAttributesToRead() + * @param array $ldapEntry the user entry as retrieved from LDAP + */ + public function processAttributes($ldapEntry) { + $this->markRefreshTime(); + //Quota + $attr = strtolower($this->connection->ldapQuotaAttribute); + if(isset($ldapEntry[$attr])) { + $this->updateQuota($ldapEntry[$attr][0]); + } + unset($attr); + + //Email + $attr = strtolower($this->connection->ldapEmailAttribute); + if(isset($ldapEntry[$attr])) { + $this->updateEmail($ldapEntry[$attr][0]); + } + unset($attr); + + //displayName + $attr = strtolower($this->connection->ldapUserDisplayName); + if(isset($ldapEntry[$attr])) { + $displayName = $ldapEntry[$attr][0]; + if(!empty($displayName)) { + $this->storeDisplayName($displayName); + $this->access->cacheUserDisplayName($this->getUsername(), $displayName); + } + } + unset($attr); + + // LDAP Username, needed for s2s sharing + if(isset($ldapEntry['uid'])) { + $this->storeLDAPUserName($ldapEntry['uid'][0]); + } else if(isset($ldapEntry['samaccountname'])) { + $this->storeLDAPUserName($ldapEntry['samaccountname'][0]); + } + + //homePath + if(strpos($this->connection->homeFolderNamingRule, 'attr:') === 0) { + $attr = strtolower(substr($this->connection->homeFolderNamingRule, strlen('attr:'))); + if(isset($ldapEntry[$attr])) { + $this->access->cacheUserHome( + $this->getUsername(), $this->getHomePath($ldapEntry[$attr][0])); + } + } + + //memberOf groups + $cacheKey = 'getMemberOf'.$this->getUsername(); + $groups = false; + if(isset($ldapEntry['memberof'])) { + $groups = $ldapEntry['memberof']; + } + $this->connection->writeToCache($cacheKey, $groups); + + //Avatar + $attrs = array('jpegphoto', 'thumbnailphoto'); + foreach ($attrs as $attr) { + if(isset($ldapEntry[$attr])) { + $this->avatarImage = $ldapEntry[$attr][0]; + $this->updateAvatar(); + break; + } + } + } + /** * @brief returns the LDAP DN of the user * @return string @@ -154,6 +220,68 @@ public function getUsername() { return $this->uid; } + /** + * returns the home directory of the user if specified by LDAP settings + * @param string $valueFromLDAP + * @return bool|string + * @throws \Exception + */ + public function getHomePath($valueFromLDAP = null) { + $path = $valueFromLDAP; + $attr = null; + + if( is_null($path) + && strpos($this->access->connection->homeFolderNamingRule, 'attr:') === 0 + && $this->access->connection->homeFolderNamingRule !== 'attr:') + { + $attr = substr($this->access->connection->homeFolderNamingRule, strlen('attr:')); + $homedir = $this->access->readAttribute( + $this->access->username2dn($this->getUsername()), $attr); + if ($homedir && isset($homedir[0])) { + $path = $homedir[0]; + } + } + + if(!empty($path)) { + //if attribute's value is an absolute path take this, otherwise append it to data dir + //check for / at the beginning or pattern c:\ resp. c:/ + if( '/' !== $path[0] + && !(3 < strlen($path) && ctype_alpha($path[0]) + && $path[1] === ':' && ('\\' === $path[2] || '/' === $path[2])) + ) { + $path = $this->config->getSystemValue('datadirectory', + \OC::$SERVERROOT.'/data' ) . '/' . $path; + } + //we need it to store it in the DB as well in case a user gets + //deleted so we can clean up afterwards + $this->config->setUserValue( + $this->getUsername(), 'user_ldap', 'homePath', $path + ); + return $path; + } + + if( !is_null($attr) + && $this->config->getAppValue('user_ldap', 'enforce_home_folder_naming_rule', true) + ) { + // a naming rule attribute is defined, but it doesn't exist for that LDAP user + throw new \Exception('Home dir attribute can\'t be read from LDAP for uid: ' . $this->getUsername()); + } + + //false will apply default behaviour as defined and done by OC_User + $this->config->setUserValue($this->getUsername(), 'user_ldap', 'homePath', ''); + return false; + } + + public function getMemberOfGroups() { + $cacheKey = 'getMemberOf'.$this->getUsername(); + if($this->connection->isCached($cacheKey)) { + return $this->connection->getFromCache($cacheKey); + } + $groupDNs = $this->access->readAttribute($this->getDN(), 'memberOf'); + $this->connection->writeToCache($cacheKey, $groupDNs); + return $groupDNs; + } + /** * @brief reads the image from LDAP that shall be used as Avatar * @return string data (provided by LDAP) | false @@ -189,7 +317,7 @@ public function markLogin() { * @brief marks the time when user features like email have been updated * @return null */ - private function markRefreshTime() { + public function markRefreshTime() { $this->config->setUserValue( $this->uid, 'user_ldap', self::USER_PREFKEY_LASTREFRESH, time()); } @@ -252,48 +380,52 @@ private function wasRefreshed($feature) { } /** - * @brief fetches the email from LDAP and stores it as ownCloud user value + * fetches the email from LDAP and stores it as ownCloud user value + * @param string $valueFromLDAP if known, to save an LDAP read request * @return null */ - public function updateEmail() { + public function updateEmail($valueFromLDAP = null) { if($this->wasRefreshed('email')) { return; } - - $email = null; - $emailAttribute = $this->connection->ldapEmailAttribute; - if(!empty($emailAttribute)) { - $aEmail = $this->access->readAttribute($this->dn, $emailAttribute); - if($aEmail && (count($aEmail) > 0)) { - $email = $aEmail[0]; - } - if(!is_null($email)) { - $this->config->setUserValue( - $this->uid, 'settings', 'email', $email); + $email = $valueFromLDAP; + if(is_null($valueFromLDAP)) { + $emailAttribute = $this->connection->ldapEmailAttribute; + if(!empty($emailAttribute)) { + $aEmail = $this->access->readAttribute($this->dn, $emailAttribute); + if(is_array($aEmail) && (count($aEmail) > 0)) { + $email = $aEmail[0]; + } } } + if(!is_null($email)) { + $this->config->setUserValue( + $this->uid, 'settings', 'email', $email); + } } /** - * @brief fetches the quota from LDAP and stores it as ownCloud user value + * fetches the quota from LDAP and stores it as ownCloud user value + * @param string $valueFromLDAP the quota attribute's value can be passed, + * to save the readAttribute request * @return null */ - public function updateQuota() { + public function updateQuota($valueFromLDAP = null) { if($this->wasRefreshed('quota')) { return; } - - $quota = null; + //can be null $quotaDefault = $this->connection->ldapQuotaDefault; - $quotaAttribute = $this->connection->ldapQuotaAttribute; - if(!empty($quotaDefault)) { - $quota = $quotaDefault; - } - if(!empty($quotaAttribute)) { - $aQuota = $this->access->readAttribute($this->dn, $quotaAttribute); - - if($aQuota && (count($aQuota) > 0)) { - $quota = $aQuota[0]; + $quota = $quotaDefault !== '' ? $quotaDefault : null; + $quota = !is_null($valueFromLDAP) ? $valueFromLDAP : $quota; + + if(is_null($valueFromLDAP)) { + $quotaAttribute = $this->connection->ldapQuotaAttribute; + if(!empty($quotaAttribute)) { + $aQuota = $this->access->readAttribute($this->dn, $quotaAttribute); + if($aQuota && (count($aQuota) > 0)) { + $quota = $aQuota[0]; + } } } if(!is_null($quota)) { diff --git a/apps/user_ldap/lib/wizard.php b/apps/user_ldap/lib/wizard.php index 6ca84c8718fe..7bf191e3be56 100644 --- a/apps/user_ldap/lib/wizard.php +++ b/apps/user_ldap/lib/wizard.php @@ -434,7 +434,11 @@ public function fetchGroups($dbKey, $confKey) { // detection will fail later $result = $this->access->searchGroups($filter, array('cn', 'dn'), $limit, $offset); foreach($result as $item) { - $groupNames[] = $item['cn']; + if(!isset($item['cn']) && !is_array($item['cn']) && !isset($item['cn'][0])) { + // just in case - no issue known + continue; + } + $groupNames[] = $item['cn'][0]; $groupEntries[] = $item; } $offset += $limit; diff --git a/apps/user_ldap/tests/access.php b/apps/user_ldap/tests/access.php index c4cd2036461c..5c535720fec5 100644 --- a/apps/user_ldap/tests/access.php +++ b/apps/user_ldap/tests/access.php @@ -29,7 +29,7 @@ use \OCA\user_ldap\lib\ILDAPWrapper; class Test_Access extends \Test\TestCase { - private function getConnecterAndLdapMock() { + private function getConnectorAndLdapMock() { static $conMethods; static $accMethods; static $umMethods; @@ -56,7 +56,7 @@ private function getConnecterAndLdapMock() { } public function testEscapeFilterPartValidChars() { - list($lw, $con, $um) = $this->getConnecterAndLdapMock(); + list($lw, $con, $um) = $this->getConnectorAndLdapMock(); $access = new Access($con, $lw, $um); $input = 'okay'; @@ -64,7 +64,7 @@ public function testEscapeFilterPartValidChars() { } public function testEscapeFilterPartEscapeWildcard() { - list($lw, $con, $um) = $this->getConnecterAndLdapMock(); + list($lw, $con, $um) = $this->getConnectorAndLdapMock(); $access = new Access($con, $lw, $um); $input = '*'; @@ -73,7 +73,7 @@ public function testEscapeFilterPartEscapeWildcard() { } public function testEscapeFilterPartEscapeWildcard2() { - list($lw, $con, $um) = $this->getConnecterAndLdapMock(); + list($lw, $con, $um) = $this->getConnectorAndLdapMock(); $access = new Access($con, $lw, $um); $input = 'foo*bar'; @@ -83,7 +83,7 @@ public function testEscapeFilterPartEscapeWildcard2() { /** @dataProvider convertSID2StrSuccessData */ public function testConvertSID2StrSuccess(array $sidArray, $sidExpected) { - list($lw, $con, $um) = $this->getConnecterAndLdapMock(); + list($lw, $con, $um) = $this->getConnectorAndLdapMock(); $access = new Access($con, $lw, $um); $sidBinary = implode('', $sidArray); @@ -118,7 +118,7 @@ public function convertSID2StrSuccessData() { } public function testConvertSID2StrInputError() { - list($lw, $con, $um) = $this->getConnecterAndLdapMock(); + list($lw, $con, $um) = $this->getConnectorAndLdapMock(); $access = new Access($con, $lw, $um); $sidIllegal = 'foobar'; @@ -128,7 +128,7 @@ public function testConvertSID2StrInputError() { } public function testGetDomainDNFromDNSuccess() { - list($lw, $con, $um) = $this->getConnecterAndLdapMock(); + list($lw, $con, $um) = $this->getConnectorAndLdapMock(); $access = new Access($con, $lw, $um); $inputDN = 'uid=zaphod,cn=foobar,dc=my,dc=server,dc=com'; @@ -143,7 +143,7 @@ public function testGetDomainDNFromDNSuccess() { } public function testGetDomainDNFromDNError() { - list($lw, $con, $um) = $this->getConnecterAndLdapMock(); + list($lw, $con, $um) = $this->getConnectorAndLdapMock(); $access = new Access($con, $lw, $um); $inputDN = 'foobar'; @@ -178,7 +178,7 @@ private function getResemblesDNInputData() { } public function testStringResemblesDN() { - list($lw, $con, $um) = $this->getConnecterAndLdapMock(); + list($lw, $con, $um) = $this->getConnectorAndLdapMock(); $access = new Access($con, $lw, $um); $cases = $this->getResemblesDNInputData(); @@ -199,7 +199,7 @@ public function testStringResemblesDN() { } public function testStringResemblesDNLDAPmod() { - list($lw, $con, $um) = $this->getConnecterAndLdapMock(); + list($lw, $con, $um) = $this->getConnectorAndLdapMock(); $lw = new \OCA\user_ldap\lib\LDAP(); $access = new Access($con, $lw, $um); @@ -213,4 +213,61 @@ public function testStringResemblesDNLDAPmod() { $this->assertSame($case['expectedResult'], $access->stringResemblesDN($case['input'])); } } + + public function testCacheUserHome() { + list($lw, $con, $um) = $this->getConnectorAndLdapMock(); + $access = new Access($con, $lw, $um); + + $con->expects($this->once()) + ->method('writeToCache'); + + $access->cacheUserHome('foobar', '/foobars/path'); + } + + public function testBatchApplyUserAttributes() { + list($lw, $con, $um) = $this->getConnectorAndLdapMock(); + $access = new Access($con, $lw, $um); + $mapperMock = $this->getMockBuilder('\OCA\User_LDAP\Mapping\UserMapping') + ->disableOriginalConstructor() + ->getMock(); + + $mapperMock->expects($this->any()) + ->method('getNameByDN') + ->will($this->returnValue('a_username')); + + $userMock = $this->getMockBuilder('\OCA\user_ldap\lib\user\User') + ->disableOriginalConstructor() + ->getMock(); + + $access->connection->expects($this->any()) + ->method('__get') + ->will($this->returnValue('displayName')); + + $access->setUserMapper($mapperMock); + + $displayNameAttribute = strtolower($access->connection->ldapUserDisplayName); + $data = array( + array( + 'dn' => 'foobar', + $displayNameAttribute => 'barfoo' + ), + array( + 'dn' => 'foo', + $displayNameAttribute => 'bar' + ), + array( + 'dn' => 'raboof', + $displayNameAttribute => 'oofrab' + ) + ); + + $userMock->expects($this->exactly(count($data))) + ->method('processAttributes'); + + $um->expects($this->exactly(count($data))) + ->method('get') + ->will($this->returnValue($userMock)); + + $access->batchApplyUserAttributes($data); + } } diff --git a/apps/user_ldap/tests/group_ldap.php b/apps/user_ldap/tests/group_ldap.php index 805238e7d379..5d2764a69d99 100644 --- a/apps/user_ldap/tests/group_ldap.php +++ b/apps/user_ldap/tests/group_ldap.php @@ -41,18 +41,17 @@ private function getAccessMock() { $connector = $this->getMock('\OCA\user_ldap\lib\Connection', $conMethods, array($lw, null, null)); - $um = new \OCA\user_ldap\lib\user\Manager( - $this->getMock('\OCP\IConfig'), - $this->getMock('\OCA\user_ldap\lib\FilesystemHelper'), - $this->getMock('\OCA\user_ldap\lib\LogWrapper'), - $this->getMock('\OCP\IAvatarManager'), - $this->getMock('\OCP\Image'), - $this->getMock('\OCP\IDBConnection') - ); + $um = $this->getMockBuilder('\OCA\user_ldap\lib\user\Manager') + ->disableOriginalConstructor() + ->getMock(); $access = $this->getMock('\OCA\user_ldap\lib\Access', $accMethods, array($connector, $lw, $um)); + $access->expects($this->any()) + ->method('getConnection') + ->will($this->returnValue($connector)); + return $access; } @@ -140,7 +139,7 @@ public function testPrimaryGroupID2NameSuccess() { $access->expects($this->once()) ->method('searchGroups') - ->will($this->returnValue(array('cn=foo,dc=barfoo,dc=bar'))); + ->will($this->returnValue([['dn' => ['cn=foo,dc=barfoo,dc=bar']]])); $access->expects($this->once()) ->method('dn2groupname') @@ -216,7 +215,7 @@ public function testPrimaryGroupID2NameNoName() { $access->expects($this->once()) ->method('searchGroups') - ->will($this->returnValue(array('cn=foo,dc=barfoo,dc=bar'))); + ->will($this->returnValue([['dn' => ['cn=foo,dc=barfoo,dc=bar']]])); $access->expects($this->once()) ->method('dn2groupname') @@ -391,7 +390,7 @@ public function testGetUserGroupsMemberOf() { $access->connection->hasPrimaryGroups = false; - $access->expects($this->once()) + $access->expects($this->any()) ->method('username2dn') ->will($this->returnValue($dn)); diff --git a/apps/user_ldap/tests/user/manager.php b/apps/user_ldap/tests/user/manager.php index d659323fc7f8..98e48638d8b2 100644 --- a/apps/user_ldap/tests/user/manager.php +++ b/apps/user_ldap/tests/user/manager.php @@ -37,6 +37,16 @@ private function getTestInstances() { $image = $this->getMock('\OCP\Image'); $dbc = $this->getMock('\OCP\IDBConnection'); + $connection = new \OCA\user_ldap\lib\Connection( + $lw = $this->getMock('\OCA\user_ldap\lib\ILDAPWrapper'), + '', + null + ); + + $access->expects($this->any()) + ->method('getConnection') + ->will($this->returnValue($connection)); + return array($access, $config, $filesys, $image, $log, $avaMgr, $dbc); } @@ -206,4 +216,36 @@ public function testGetByUidNotExisting() { $this->assertNull($user); } + public function testGetAttributesAll() { + list($access, $config, $filesys, $image, $log, $avaMgr, $dbc) = + $this->getTestInstances(); + + $manager = new Manager($config, $filesys, $log, $avaMgr, $image, $dbc); + $manager->setLdapAccess($access); + + $connection = $access->getConnection(); + $connection->setConfiguration(array('ldapEmailAttribute' => 'mail')); + + $attributes = $manager->getAttributes(); + + $this->assertTrue(in_array('dn', $attributes)); + $this->assertTrue(in_array($access->getConnection()->ldapEmailAttribute, $attributes)); + $this->assertTrue(in_array('jpegphoto', $attributes)); + $this->assertTrue(in_array('thumbnailphoto', $attributes)); + } + + public function testGetAttributesMinimal() { + list($access, $config, $filesys, $image, $log, $avaMgr, $dbc) = + $this->getTestInstances(); + + $manager = new Manager($config, $filesys, $log, $avaMgr, $image, $dbc); + $manager->setLdapAccess($access); + + $attributes = $manager->getAttributes(true); + + $this->assertTrue(in_array('dn', $attributes)); + $this->assertTrue(!in_array('jpegphoto', $attributes)); + $this->assertTrue(!in_array('thumbnailphoto', $attributes)); + } + } diff --git a/apps/user_ldap/tests/user/user.php b/apps/user_ldap/tests/user/user.php index 80baafcb2504..19581d835d1f 100644 --- a/apps/user_ldap/tests/user/user.php +++ b/apps/user_ldap/tests/user/user.php @@ -221,7 +221,7 @@ public function testUpdateQuotaDefaultProvided() { $connection->expects($this->at(0)) ->method('__get') ->with($this->equalTo('ldapQuotaDefault')) - ->will($this->returnValue('23 GB')); + ->will($this->returnValue('25 GB')); $connection->expects($this->at(1)) ->method('__get') @@ -242,7 +242,7 @@ public function testUpdateQuotaDefaultProvided() { ->with($this->equalTo('alice'), $this->equalTo('files'), $this->equalTo('quota'), - $this->equalTo('23 GB')) + $this->equalTo('25 GB')) ->will($this->returnValue(true)); $uid = 'alice'; @@ -278,14 +278,14 @@ public function testUpdateQuotaIndividualProvided() { ->method('readAttribute') ->with($this->equalTo('uid=alice,dc=foo,dc=bar'), $this->equalTo('myquota')) - ->will($this->returnValue(array('23 GB'))); + ->will($this->returnValue(array('27 GB'))); $config->expects($this->once()) ->method('setUserValue') ->with($this->equalTo('alice'), $this->equalTo('files'), $this->equalTo('quota'), - $this->equalTo('23 GB')) + $this->equalTo('27 GB')) ->will($this->returnValue(true)); $uid = 'alice'; @@ -370,6 +370,45 @@ public function testUpdateQuotaNoneConfigured() { $user->updateQuota(); } + public function testUpdateQuotaFromValue() { + list($access, $config, $filesys, $image, $log, $avaMgr, $dbc) = + $this->getTestInstances(); + + list($access, $connection) = + $this->getAdvancedMocks($config, $filesys, $log, $avaMgr, $dbc); + + $readQuota = '19 GB'; + + $connection->expects($this->at(0)) + ->method('__get') + ->with($this->equalTo('ldapQuotaDefault')) + ->will($this->returnValue('')); + + $connection->expects($this->once(1)) + ->method('__get') + ->with($this->equalTo('ldapQuotaDefault')) + ->will($this->returnValue(null)); + + $access->expects($this->never()) + ->method('readAttribute'); + + $config->expects($this->once()) + ->method('setUserValue') + ->with($this->equalTo('alice'), + $this->equalTo('files'), + $this->equalTo('quota'), + $this->equalTo($readQuota)) + ->will($this->returnValue(true)); + + $uid = 'alice'; + $dn = 'uid=alice,dc=foo,dc=bar'; + + $user = new User( + $uid, $dn, $access, $config, $filesys, $image, $log, $avaMgr); + + $user->updateQuota($readQuota); + } + //the testUpdateAvatar series also implicitely tests getAvatarImage public function testUpdateAvatarJpegPhotoProvided() { list($access, $config, $filesys, $image, $log, $avaMgr, $dbc) = @@ -679,4 +718,164 @@ public function testGetAvatarImageProvided() { //photo is returned $photo = $user->getAvatarImage(); } + + public function testProcessAttributes() { + list(, $config, $filesys, $image, $log, $avaMgr, $dbc) = + $this->getTestInstances(); + + list($access, $connection) = + $this->getAdvancedMocks($config, $filesys, $log, $avaMgr, $dbc); + + $uid = 'alice'; + $dn = 'uid=alice'; + + $requiredMethods = array( + 'markRefreshTime', + 'updateQuota', + 'updateEmail', + 'storeDisplayName', + 'storeLDAPUserName', + 'getHomePath', + 'updateAvatar' + ); + + $userMock = $this->getMockBuilder('OCA\user_ldap\lib\user\User') + ->setConstructorArgs(array($uid, $dn, $access, $config, $filesys, $image, $log, $avaMgr)) + ->setMethods($requiredMethods) + ->getMock(); + + $connection->setConfiguration(array( + 'homeFolderNamingRule' => 'homeDirectory' + )); + + $connection->expects($this->any()) + ->method('__get') + //->will($this->returnArgument(0)); + ->will($this->returnCallback(function($name) { + if($name === 'homeFolderNamingRule') { + return 'attr:homeDirectory'; + } + return $name; + })); + + $record = array( + strtolower($connection->ldapQuotaAttribute) => array('4096'), + strtolower($connection->ldapEmailAttribute) => array('alice@wonderland.org'), + strtolower($connection->ldapUserDisplayName) => array('Aaaaalice'), + 'uid' => array($uid), + 'homedirectory' => array('Alice\'s Folder'), + 'memberof' => array('cn=groupOne', 'cn=groupTwo'), + 'jpegphoto' => array('here be an image') + ); + + foreach($requiredMethods as $method) { + $userMock->expects($this->once()) + ->method($method); + } + + $userMock->processAttributes($record); + } + + public function emptyHomeFolderAttributeValueProvider() { + return array( + 'empty' => array(''), + 'prefixOnly' => array('attr:'), + ); + } + + /** + * @dataProvider emptyHomeFolderAttributeValueProvider + */ + public function testGetHomePathNotConfigured($attributeValue) { + list($access, $config, $filesys, $image, $log, $avaMgr, $dbc) = + $this->getTestInstances(); + + list($access, $connection) = + $this->getAdvancedMocks($config, $filesys, $log, $avaMgr, $dbc); + + $connection->expects($this->any()) + ->method('__get') + ->with($this->equalTo('homeFolderNamingRule')) + ->will($this->returnValue($attributeValue)); + + $access->expects($this->never()) + ->method('readAttribute'); + + $config->expects($this->never()) + ->method('getAppValue'); + + $uid = 'alice'; + $dn = 'uid=alice,dc=foo,dc=bar'; + + $user = new User( + $uid, $dn, $access, $config, $filesys, $image, $log, $avaMgr); + + $path = $user->getHomePath(); + $this->assertSame($path, false); + } + + public function testGetHomePathConfiguredNotAvailableAllowed() { + list($access, $config, $filesys, $image, $log, $avaMgr, $dbc) = + $this->getTestInstances(); + + list($access, $connection) = + $this->getAdvancedMocks($config, $filesys, $log, $avaMgr, $dbc); + + $connection->expects($this->any()) + ->method('__get') + ->with($this->equalTo('homeFolderNamingRule')) + ->will($this->returnValue('attr:foobar')); + + $access->expects($this->once()) + ->method('readAttribute') + ->will($this->returnValue(false)); + + // asks for "enforce_home_folder_naming_rule" + $config->expects($this->once()) + ->method('getAppValue') + ->will($this->returnValue(false)); + + $uid = 'alice'; + $dn = 'uid=alice,dc=foo,dc=bar'; + + $user = new User( + $uid, $dn, $access, $config, $filesys, $image, $log, $avaMgr); + + $path = $user->getHomePath(); + + $this->assertSame($path, false); + } + + /** + * @expectedException \Exception + */ + public function testGetHomePathConfiguredNotAvailableNotAllowed() { + list($access, $config, $filesys, $image, $log, $avaMgr, $dbc) = + $this->getTestInstances(); + + list($access, $connection) = + $this->getAdvancedMocks($config, $filesys, $log, $avaMgr, $dbc); + + $connection->expects($this->any()) + ->method('__get') + ->with($this->equalTo('homeFolderNamingRule')) + ->will($this->returnValue('attr:foobar')); + + $access->expects($this->once()) + ->method('readAttribute') + ->will($this->returnValue(false)); + + // asks for "enforce_home_folder_naming_rule" + $config->expects($this->once()) + ->method('getAppValue') + ->will($this->returnValue(true)); + + $uid = 'alice'; + $dn = 'uid=alice,dc=foo,dc=bar'; + + $user = new User( + $uid, $dn, $access, $config, $filesys, $image, $log, $avaMgr); + + $user->getHomePath(); + } } diff --git a/apps/user_ldap/tests/user_ldap.php b/apps/user_ldap/tests/user_ldap.php index 2b99e1c2dc8e..0f70c43fc11f 100644 --- a/apps/user_ldap/tests/user_ldap.php +++ b/apps/user_ldap/tests/user_ldap.php @@ -34,6 +34,7 @@ class Test_User_Ldap_Direct extends \Test\TestCase { protected $backend; protected $access; + protected $configMock; protected function setUp() { parent::setUp(); @@ -61,8 +62,9 @@ private function getAccessMock() { $conMethods, array($lw, null, null)); + $this->configMock = $this->getMock('\OCP\IConfig'); $um = new \OCA\user_ldap\lib\user\Manager( - $this->getMock('\OCP\IConfig'), + $this->configMock, $this->getMock('\OCA\user_ldap\lib\FilesystemHelper'), $this->getMock('\OCA\user_ldap\lib\LogWrapper'), $this->getMock('\OCP\IAvatarManager'), @@ -122,7 +124,7 @@ private function prepareAccessForCheckPassword(&$access, $noDisplayName = false) ->method('fetchListOfUsers') ->will($this->returnCallback(function($filter) { if($filter === 'roland') { - return array(array('dn' => 'dnOfRoland,dc=test')); + return array(array('dn' => ['dnOfRoland,dc=test'])); } return array(); })); @@ -131,7 +133,7 @@ private function prepareAccessForCheckPassword(&$access, $noDisplayName = false) ->method('fetchUsersByLoginName') ->will($this->returnCallback(function($uid) { if($uid === 'roland') { - return array(array('dn' => 'dnOfRoland,dc=test')); + return array(array('dn' => ['dnOfRoland,dc=test'])); } return array(); })); @@ -586,6 +588,13 @@ public function testGetHomeRelative() { $backend = new UserLDAP($access, $config); $this->prepareMockForUserExists($access); + $dataDir = \OC::$server->getConfig()->getSystemValue( + 'datadirectory', \OC::$SERVERROOT.'/data'); + + $this->configMock->expects($this->once()) + ->method('getSystemValue') + ->will($this->returnValue($dataDir)); + $access->connection->expects($this->any()) ->method('__get') ->will($this->returnCallback(function($name) { @@ -609,14 +618,9 @@ public function testGetHomeRelative() { return false; } })); - //datadir-relativ path - $datadir = '/my/data/dir'; - $config->expects($this->once()) - ->method('getSystemValue') - ->will($this->returnValue($datadir)); $result = $backend->getHome('ladyofshadows'); - $this->assertEquals($datadir.'/susannah/', $result); + $this->assertEquals($dataDir.'/susannah/', $result); } /** diff --git a/apps/user_ldap/user_ldap.php b/apps/user_ldap/user_ldap.php index caff30a0e601..97fac7d85b54 100644 --- a/apps/user_ldap/user_ldap.php +++ b/apps/user_ldap/user_ldap.php @@ -70,6 +70,23 @@ public function canChangeAvatar($uid) { return false; } + /** + * returns an LDAP record based on a given login name + * + * @param string $loginName + * @return array + * @throws \Exception + */ + public function getLDAPUserByLoginName($loginName) { + //find out dn of the user name + $attrs = $this->access->userManager->getAttributes(); + $users = $this->access->fetchUsersByLoginName($loginName, $attrs, 1); + if(count($users) < 1) { + throw new \Exception('No user available for the given login name.'); + } + return $users[0]; + } + /** * Check if the password is correct * @param string $uid The username @@ -79,15 +96,14 @@ public function canChangeAvatar($uid) { * Check if the password is correct without logging in the user */ public function checkPassword($uid, $password) { - //find out dn of the user name - $attrs = array($this->access->connection->ldapUserDisplayName, 'dn', - 'uid', 'samaccountname'); - $users = $this->access->fetchUsersByLoginName($uid, $attrs); - if(count($users) < 1) { + try { + $ldapRecord = $this->getLDAPUserByLoginName($uid); + } catch(\Exception $e) { return false; } - $dn = $users[0]['dn']; + $dn = $ldapRecord['dn'][0]; $user = $this->access->userManager->get($dn); + if(!$user instanceof User) { \OCP\Util::writeLog('user_ldap', 'LDAP Login: Could not get user object for DN ' . $dn . @@ -101,16 +117,9 @@ public function checkPassword($uid, $password) { return false; } + $this->access->cacheUserExists($user->getUsername()); + $user->processAttributes($ldapRecord); $user->markLogin(); - if(isset($users[0][$this->access->connection->ldapUserDisplayName])) { - $dpn = $users[0][$this->access->connection->ldapUserDisplayName]; - $user->storeDisplayName($dpn); - } - if(isset($users[0]['uid'])) { - $user->storeLDAPUserName($users[0]['uid']); - } else if(isset($users[0]['samaccountname'])) { - $user->storeLDAPUserName($users[0]['samaccountname']); - } return $user->getUsername(); } @@ -152,7 +161,7 @@ public function getUsers($search = '', $limit = 10, $offset = 0) { //do the search and translate results to owncloud names $ldap_users = $this->access->fetchListOfUsers( $filter, - array($this->access->connection->ldapUserDisplayName, 'dn'), + $this->access->userManager->getAttributes(true), $limit, $offset); $ldap_users = $this->access->ownCloudUserNames($ldap_users); \OCP\Util::writeLog('user_ldap', 'getUsers: '.count($ldap_users). ' Users found', \OCP\Util::DEBUG); @@ -266,39 +275,12 @@ public function getHome($uid) { if($this->access->connection->isCached($cacheKey)) { return $this->access->connection->getFromCache($cacheKey); } - if(strpos($this->access->connection->homeFolderNamingRule, 'attr:') === 0) { - $attr = substr($this->access->connection->homeFolderNamingRule, strlen('attr:')); - $homedir = $this->access->readAttribute( - $this->access->username2dn($uid), $attr); - if($homedir && isset($homedir[0])) { - $path = $homedir[0]; - //if attribute's value is an absolute path take this, otherwise append it to data dir - //check for / at the beginning or pattern c:\ resp. c:/ - if( - '/' === $path[0] - || (3 < strlen($path) && ctype_alpha($path[0]) - && $path[1] === ':' && ('\\' === $path[2] || '/' === $path[2])) - ) { - $homedir = $path; - } else { - $homedir = $this->ocConfig->getSystemValue('datadirectory', - \OC::$SERVERROOT.'/data' ) . '/' . $homedir[0]; - } - $this->access->connection->writeToCache($cacheKey, $homedir); - //we need it to store it in the DB as well in case a user gets - //deleted so we can clean up afterwards - $this->ocConfig->setUserValue( - $uid, 'user_ldap', 'homePath', $homedir - ); - //TODO: if home directory changes, the old one needs to be removed. - return $homedir; - } - } - //false will apply default behaviour as defined and done by OC_User - $this->access->connection->writeToCache($cacheKey, false); - $this->ocConfig->setUserValue($uid, 'user_ldap', 'homePath', ''); - return false; + $user = $this->access->userManager->get($uid); + $path = $user->getHomePath(); + $this->access->cacheUserHome($uid, $path); + + return $path; } /**