diff --git a/apps/user_ldap/css/settings.css b/apps/user_ldap/css/settings.css index c99efc48c5bfc..129064ff8b058 100644 --- a/apps/user_ldap/css/settings.css +++ b/apps/user_ldap/css/settings.css @@ -3,6 +3,11 @@ width: 85%; } +.inlinetable { + display: inline-table; + vertical-align: bottom; +} + .tablerow { display: table-row; white-space: nowrap; diff --git a/apps/user_ldap/js/wizard/wizardTabAdvanced.js b/apps/user_ldap/js/wizard/wizardTabAdvanced.js index d1e5002d40aa7..d0922bbff320c 100644 --- a/apps/user_ldap/js/wizard/wizardTabAdvanced.js +++ b/apps/user_ldap/js/wizard/wizardTabAdvanced.js @@ -95,6 +95,10 @@ OCA = OCA || {}; $element: $('#ldap_paging_size'), setMethod: 'setPagingSize' }, + ldap_turn_on_pwd_change: { + $element: $('#ldap_turn_on_pwd_change'), + setMethod: 'setPasswordChangeEnabled' + }, //Special Attributes ldap_quota_attr: { @@ -288,6 +292,17 @@ OCA = OCA || {}; setPagingSize: function(size) { this.setElementValue(this.managedItems.ldap_paging_size.$element, size); }, + + /** + * sets whether the password changes per user should be enabled + * + * @param {string} doPasswordChange contains an int + */ + setPasswordChangeEnabled: function(doPasswordChange) { + this.setElementValue( + this.managedItems.ldap_turn_on_pwd_change.$element, doPasswordChange + ); + }, /** * sets the email attribute diff --git a/apps/user_ldap/lib/Access.php b/apps/user_ldap/lib/Access.php index e7facd80ae0da..f06f76bb910b5 100644 --- a/apps/user_ldap/lib/Access.php +++ b/apps/user_ldap/lib/Access.php @@ -40,6 +40,8 @@ namespace OCA\User_LDAP; +use OC\HintException; +use OCA\User_LDAP\Exceptions\ConstraintViolationException; use OCA\User_LDAP\User\IUserTools; use OCA\User_LDAP\User\Manager; use OCA\User_LDAP\User\OfflineUser; @@ -221,6 +223,30 @@ public function readAttribute($dn, $attr, $filter = 'objectClass=*') { \OCP\Util::writeLog('user_ldap', 'Requested attribute '.$attr.' not found for '.$dn, \OCP\Util::DEBUG); return false; } + + /** + * Set password for an LDAP user identified by a DN + * @param string $userDN the user in question + * @param string $password the new password + * @return bool + */ + public function setPassword($userDN, $password) { + if(intval($this->connection->turnOnPasswordChange) !== 1) { + throw new \Exception('LDAP password changes are disabled.'); + } + $cr = $this->connection->getConnectionResource(); + if(!$this->ldap->isResource($cr)) { + //LDAP not available + \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG); + return false; + } + + try { + return $this->ldap->modReplace($cr, $userDN, $password); + } catch(ConstraintViolationException $e) { + throw new HintException('Password change rejected.', \OC::$server->getL10N('user_ldap')->t('Password change rejected. Hint: ').$e->getMessage(), $e->getCode()); + } + } /** * checks whether the given attributes value is probably a DN diff --git a/apps/user_ldap/lib/Configuration.php b/apps/user_ldap/lib/Configuration.php index 80b353360c319..eb4fcd3fbe604 100644 --- a/apps/user_ldap/lib/Configuration.php +++ b/apps/user_ldap/lib/Configuration.php @@ -11,6 +11,7 @@ * @author Lukas Reschke * @author Morris Jobke * @author Robin McCorkell + * @author Roger Szabo * * @license AGPL-3.0 * @@ -90,6 +91,7 @@ class Configuration { 'lastJpegPhotoLookup' => null, 'ldapNestedGroups' => false, 'ldapPagingSize' => null, + 'turnOnPasswordChange' => false, 'ldapDynamicGroupMemberURL' => null, ); @@ -449,6 +451,7 @@ public function getDefaults() { 'last_jpegPhoto_lookup' => 0, 'ldap_nested_groups' => 0, 'ldap_paging_size' => 500, + 'ldap_turn_on_pwd_change' => 0, 'ldap_experienced_admin' => 0, 'ldap_dynamic_group_member_url' => '', ); @@ -505,6 +508,7 @@ public function getConfigTranslationArray() { 'last_jpegPhoto_lookup' => 'lastJpegPhotoLookup', 'ldap_nested_groups' => 'ldapNestedGroups', 'ldap_paging_size' => 'ldapPagingSize', + 'ldap_turn_on_pwd_change' => 'turnOnPasswordChange', 'ldap_experienced_admin' => 'ldapExperiencedAdmin', 'ldap_dynamic_group_member_url' => 'ldapDynamicGroupMemberURL', ); diff --git a/apps/user_ldap/lib/Exceptions/ConstraintViolationException.php b/apps/user_ldap/lib/Exceptions/ConstraintViolationException.php new file mode 100644 index 0000000000000..997b01b2d4e39 --- /dev/null +++ b/apps/user_ldap/lib/Exceptions/ConstraintViolationException.php @@ -0,0 +1,26 @@ + + * + * @author Roger Szabo + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 program. If not, see . + * + */ + +namespace OCA\User_LDAP\Exceptions; + +class ConstraintViolationException extends \Exception {} diff --git a/apps/user_ldap/lib/ILDAPWrapper.php b/apps/user_ldap/lib/ILDAPWrapper.php index 4fd3b31428add..e2089fa8a47d3 100644 --- a/apps/user_ldap/lib/ILDAPWrapper.php +++ b/apps/user_ldap/lib/ILDAPWrapper.php @@ -163,6 +163,15 @@ public function read($link, $baseDN, $filter, $attr); * @return resource|false an LDAP search result resource, false on error */ public function search($link, $baseDN, $filter, $attr, $attrsOnly = 0, $limit = 0); + + /** + * Replace the value of a userPassword by $password + * @param resource $link LDAP link resource + * @param string $userDN the DN of the user whose password is to be replaced + * @param string $password the new value for the userPassword + * @return bool true on success, false otherwise + */ + public function modReplace($link, $userDN, $password); /** * Sets the value of the specified option to be $value diff --git a/apps/user_ldap/lib/LDAP.php b/apps/user_ldap/lib/LDAP.php index 74d83e4ab4f11..0d491396ee406 100644 --- a/apps/user_ldap/lib/LDAP.php +++ b/apps/user_ldap/lib/LDAP.php @@ -9,6 +9,7 @@ * @author Lukas Reschke * @author Morris Jobke * @author Robin McCorkell + * @author Roger Szabo * * @license AGPL-3.0 * @@ -29,6 +30,7 @@ namespace OCA\User_LDAP; use OC\ServerNotAvailableException; +use OCA\User_LDAP\Exceptions\ConstraintViolationException; class LDAP implements ILDAPWrapper { protected $curFunc = ''; @@ -192,6 +194,16 @@ public function search($link, $baseDN, $filter, $attr, $attrsOnly = 0, $limit = return $this->invokeLDAPMethod('search', $link, $baseDN, $filter, $attr, $attrsOnly, $limit); } + /** + * @param LDAP $link + * @param string $userDN + * @param string $password + * @return bool + */ + public function modReplace($link, $userDN, $password) { + return $this->invokeLDAPMethod('mod_replace', $link, $userDN, array('userPassword' => $password)); + } + /** * @param LDAP $link * @param string $option @@ -288,6 +300,9 @@ private function postFunctionCall() { throw new \Exception('LDAP authentication method rejected', $errorCode); } else if ($errorCode === 1) { throw new \Exception('LDAP Operations error', $errorCode); + } else if ($errorCode === 19) { + ldap_get_option($this->curArgs[0], LDAP_OPT_ERROR_STRING, $extended_error); + throw new ConstraintViolationException(!empty($extended_error)?$extended_error:$errorMsg, $errorCode); } else { \OCP\Util::writeLog('user_ldap', 'LDAP error '.$errorMsg.' (' . diff --git a/apps/user_ldap/lib/User_LDAP.php b/apps/user_ldap/lib/User_LDAP.php index 9f2468bcc85a7..8dfde2d8148d8 100644 --- a/apps/user_ldap/lib/User_LDAP.php +++ b/apps/user_ldap/lib/User_LDAP.php @@ -35,6 +35,7 @@ namespace OCA\User_LDAP; +use OC\User\Backend; use OC\User\NoUserException; use OCA\User_LDAP\Exceptions\NotOnLDAP; use OCA\User_LDAP\User\OfflineUser; @@ -174,6 +175,26 @@ public function checkPassword($uid, $password) { return false; } + /** + * Set password + * @param string $uid The username + * @param string $password The new password + * @return bool + */ + public function setPassword($uid, $password) { + $user = $this->access->userManager->get($uid); + + if(!$user instanceof User) { + throw new \Exception('LDAP setPassword: Could not get user object for uid ' . $uid . + '. Maybe the LDAP entry has no set display name attribute?'); + } + if($user->getUsername() !== false) { + return $this->access->setPassword($user->getDN(), $password); + } + + return false; + } + /** * Get a list of all users * @@ -449,11 +470,12 @@ public function getDisplayNames($search = '', $limit = null, $offset = null) { * compared with OC_USER_BACKEND_CREATE_USER etc. */ public function implementsActions($actions) { - return (bool)((\OC\User\Backend::CHECK_PASSWORD - | \OC\User\Backend::GET_HOME - | \OC\User\Backend::GET_DISPLAYNAME - | \OC\User\Backend::PROVIDE_AVATAR - | \OC\User\Backend::COUNT_USERS) + return (bool)((Backend::CHECK_PASSWORD + | Backend::GET_HOME + | Backend::GET_DISPLAYNAME + | Backend::PROVIDE_AVATAR + | Backend::COUNT_USERS + | ((intval($this->access->connection->turnOnPasswordChange) === 1)?(Backend::SET_PASSWORD):0)) & $actions); } diff --git a/apps/user_ldap/lib/User_Proxy.php b/apps/user_ldap/lib/User_Proxy.php index cced469a7ae07..2cdf401880eb6 100644 --- a/apps/user_ldap/lib/User_Proxy.php +++ b/apps/user_ldap/lib/User_Proxy.php @@ -262,6 +262,17 @@ public function getDisplayNames($search = '', $limit = null, $offset = null) { public function deleteUser($uid) { return $this->handleRequest($uid, 'deleteUser', array($uid)); } + + /** + * Set password + * @param string $uid The username + * @param string $password The new password + * @return bool + * + */ + public function setPassword($uid, $password) { + return $this->handleRequest($uid, 'setPassword', array($uid, $password)); + } /** * @return bool diff --git a/apps/user_ldap/templates/settings.php b/apps/user_ldap/templates/settings.php index eb4c7b9912759..e53456c703c5b 100644 --- a/apps/user_ldap/templates/settings.php +++ b/apps/user_ldap/templates/settings.php @@ -101,6 +101,8 @@

+

t('(New password is sent as plain text to LDAP)'));?> +

t('Special Attributes'));?>

diff --git a/apps/user_ldap/tests/User_LDAPTest.php b/apps/user_ldap/tests/User_LDAPTest.php index 05837097929bc..958d0b51979a5 100644 --- a/apps/user_ldap/tests/User_LDAPTest.php +++ b/apps/user_ldap/tests/User_LDAPTest.php @@ -9,6 +9,7 @@ * @author Morris Jobke * @author Robin McCorkell * @author Thomas Müller + * @author Roger Szabo * * @license AGPL-3.0 * @@ -36,7 +37,7 @@ use OCA\User_LDAP\LogWrapper; use OCA\User_LDAP\User\Manager; use OCA\User_LDAP\User\OfflineUser; -use OCA\User_LDAP\User\User; +use OC\HintException; use OCA\User_LDAP\User_LDAP as UserLDAP; use OCP\IAvatarManager; use OCP\IConfig; @@ -958,5 +959,103 @@ public function testLoginName2UserNameOfflineUser() { // and once again to verify that caching works $backend->loginName2UserName($loginName); } + + /** + * Prepares the Access mock for setPassword tests + * @param \OCA\User_LDAP\Access|\PHPUnit_Framework_MockObject_MockObject $access mock + * @return void + */ + private function prepareAccessForSetPassword(&$access, $enablePasswordChange = true) { + $access->connection->expects($this->any()) + ->method('__get') + ->will($this->returnCallback(function($name) use (&$enablePasswordChange) { + if($name === 'ldapLoginFilter') { + return '%uid'; + } + if($name === 'turnOnPasswordChange') { + return $enablePasswordChange?1:0; + } + return null; + })); + + $access->connection->expects($this->any()) + ->method('getFromCache') + ->will($this->returnCallback(function($uid) { + if($uid === 'userExists'.'roland') { + return true; + } + return null; + })); + + $access->expects($this->any()) + ->method('fetchListOfUsers') + ->will($this->returnCallback(function($filter) { + if($filter === 'roland') { + return array(array('dn' => ['dnOfRoland,dc=test'])); + } + return array(); + })); + + $access->expects($this->any()) + ->method('fetchUsersByLoginName') + ->will($this->returnCallback(function($uid) { + if($uid === 'roland') { + return array(array('dn' => ['dnOfRoland,dc=test'])); + } + return array(); + })); + + $access->expects($this->any()) + ->method('dn2username') + ->with($this->equalTo('dnOfRoland,dc=test')) + ->will($this->returnValue('roland')); + + $access->expects($this->any()) + ->method('stringResemblesDN') + ->with($this->equalTo('dnOfRoland,dc=test')) + ->will($this->returnValue(true)); + + $access->expects($this->any()) + ->method('setPassword') + ->will($this->returnCallback(function($uid, $password) { + if(strlen($password) <= 5) { + throw new HintException('Password fails quality checking policy', '', 19); + } + return true; + })); + } + + /** + * @expectedException \OC\HintException + * @expectedExceptionMessage Password fails quality checking policy + */ + public function testSetPasswordInvalid() { + $access = $this->getAccessMock(); + $this->prepareAccessForSetPassword($access); + $backend = new UserLDAP($access, $this->createMock(IConfig::class)); + \OC_User::useBackend($backend); + + $this->assertTrue(\OC_User::setPassword('roland', 'dt')); + } + + public function testSetPasswordValid() { + $access = $this->getAccessMock(); + + $this->prepareAccessForSetPassword($access); + $backend = new UserLDAP($access, $this->createMock(IConfig::class)); + \OC_User::useBackend($backend); + + $this->assertTrue(\OC_User::setPassword('roland', 'dt12234$')); + } + + public function testSetPasswordValidDisabled() { + $access = $this->getAccessMock(); + + $this->prepareAccessForSetPassword($access, false); + $backend = new UserLDAP($access, $this->createMock(IConfig::class)); + \OC_User::useBackend($backend); + + $this->assertFalse(\OC_User::setPassword('roland', 'dt12234$')); + } }