diff --git a/apps/user_ldap/css/settings.css b/apps/user_ldap/css/settings.css index c99efc48c5bfcd864444030660fd0f22e8757936..129064ff8b058243f9b2f6d2b31825ef6256a405 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 d1e5002d40aa781f9d64a763b83f83356466ae8a..d0922bbff320c841109d5655487022b7307c16ef 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 e7facd80ae0daa45af6ecf088a97afcda2d6487a..f06f76bb910b5eb371886b2f459bd07a1ca5be47 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 @@ class Access extends LDAPUtility implements IUserTools { \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 80b353360c31914b90def2e5d58cd8b51e2c3aa7..eb4fcd3fbe6044e52ef2a846c5657d51b1b80753 100644 --- a/apps/user_ldap/lib/Configuration.php +++ b/apps/user_ldap/lib/Configuration.php @@ -11,6 +11,7 @@ * @author Lukas Reschke <lukas@statuscode.ch> * @author Morris Jobke <hey@morrisjobke.de> * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Roger Szabo <roger.szabo@web.de> * * @license AGPL-3.0 * @@ -90,6 +91,7 @@ class Configuration { 'lastJpegPhotoLookup' => null, 'ldapNestedGroups' => false, 'ldapPagingSize' => null, + 'turnOnPasswordChange' => false, 'ldapDynamicGroupMemberURL' => null, ); @@ -449,6 +451,7 @@ class Configuration { '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 @@ class Configuration { '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 0000000000000000000000000000000000000000..997b01b2d4e39fbc2ed872f4684ed49727f7b995 --- /dev/null +++ b/apps/user_ldap/lib/Exceptions/ConstraintViolationException.php @@ -0,0 +1,26 @@ +<?php +/** + * @copyright Copyright (c) 2016 Roger Szabo <roger.szabo@web.de> + * + * @author Roger Szabo <roger.szabo@web.de> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +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 4fd3b31428add863bee76a796c6b8fa524710dd2..e2089fa8a47d33108e99f59cfcede23206838e19 100644 --- a/apps/user_ldap/lib/ILDAPWrapper.php +++ b/apps/user_ldap/lib/ILDAPWrapper.php @@ -163,6 +163,15 @@ interface ILDAPWrapper { * @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 74d83e4ab4f1151afa86f2971dbf5b2f0856fbeb..0d491396ee4061351496d301037b97c356ed766e 100644 --- a/apps/user_ldap/lib/LDAP.php +++ b/apps/user_ldap/lib/LDAP.php @@ -9,6 +9,7 @@ * @author Lukas Reschke <lukas@statuscode.ch> * @author Morris Jobke <hey@morrisjobke.de> * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Roger Szabo <roger.szabo@web.de> * * @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 @@ class LDAP implements ILDAPWrapper { 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 @@ class LDAP implements ILDAPWrapper { 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 9f2468bcc85a77c534ec906241e2a17d8cb28c63..8dfde2d8148d84ee9e360aa0e5b35ac9df823fa6 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 @@ class User_LDAP extends BackendUtility implements \OCP\IUserBackend, \OCP\UserIn 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 @@ class User_LDAP extends BackendUtility implements \OCP\IUserBackend, \OCP\UserIn * 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 cced469a7ae071eff5dcbfc3e7e56f2c179d1cc0..2cdf401880eb69626016bfc66046816e57566838 100644 --- a/apps/user_ldap/lib/User_Proxy.php +++ b/apps/user_ldap/lib/User_Proxy.php @@ -262,6 +262,17 @@ class User_Proxy extends Proxy implements \OCP\IUserBackend, \OCP\UserInterface, 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 eb4c7b99127597dff2854634dfaa6d601579d24a..e53456c703c5b5b0b4a4074409411dfde6842585 100644 --- a/apps/user_ldap/templates/settings.php +++ b/apps/user_ldap/templates/settings.php @@ -101,6 +101,8 @@ style('user_ldap', 'settings'); <p><label for="ldap_dynamic_group_member_url"><?php p($l->t('Dynamic Group Member URL'));?></label><input type="text" id="ldap_dynamic_group_member_url" name="ldap_dynamic_group_member_url" title="<?php p($l->t('The LDAP attribute that on group objects contains an LDAP search URL that determines what objects belong to the group. (An empty setting disables dynamic group membership functionality.)'));?>" data-default="<?php p($_['ldap_dynamic_group_member_url_default']); ?>" /></p> <p><label for="ldap_nested_groups"><?php p($l->t('Nested Groups'));?></label><input type="checkbox" id="ldap_nested_groups" name="ldap_nested_groups" value="1" data-default="<?php p($_['ldap_nested_groups_default']); ?>" title="<?php p($l->t('When switched on, groups that contain groups are supported. (Only works if the group member attribute contains DNs.)'));?>" /></p> <p><label for="ldap_paging_size"><?php p($l->t('Paging chunksize'));?></label><input type="number" id="ldap_paging_size" name="ldap_paging_size" title="<?php p($l->t('Chunksize used for paged LDAP searches that may return bulky results like user or group enumeration. (Setting it 0 disables paged LDAP searches in those situations.)'));?>" data-default="<?php p($_['ldap_paging_size_default']); ?>" /></p> + <p><label for="ldap_turn_on_pwd_change"><?php p($l->t('Enable LDAP password changes per user'));?></label><span class="inlinetable"><span class="tablerow left"><input type="checkbox" id="ldap_turn_on_pwd_change" name="ldap_turn_on_pwd_change" value="1" data-default="<?php p($_['ldap_turn_on_pwd_change_default']); ?>" title="<?php p($l->t('Allow LDAP users to change their password and allow Super Administrators and Group Administrators to change the password of their LDAP users. Only works when access control policies are configured accordingly on the LDAP server. As passwords are sent in plaintext to the LDAP server, transport encryption must be used and password hashing should be configured on the LDAP server.'));?>" /><span class="tablecell"><?php p($l->t('(New password is sent as plain text to LDAP)'));?></span></span> + </span><br/></p> </div> <h3><?php p($l->t('Special Attributes'));?></h3> <div> diff --git a/apps/user_ldap/tests/User_LDAPTest.php b/apps/user_ldap/tests/User_LDAPTest.php index 05837097929bc10409c3aad614be34e2d3562494..958d0b51979a5a92dee41904f79e02f7506d413f 100644 --- a/apps/user_ldap/tests/User_LDAPTest.php +++ b/apps/user_ldap/tests/User_LDAPTest.php @@ -9,6 +9,7 @@ * @author Morris Jobke <hey@morrisjobke.de> * @author Robin McCorkell <robin@mccorkell.me.uk> * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Roger Szabo <roger.szabo@web.de> * * @license AGPL-3.0 * @@ -36,7 +37,7 @@ use OCA\User_LDAP\ILDAPWrapper; 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 @@ class User_LDAPTest extends TestCase { // 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$')); + } }