Merge pull request #15964 from nextcloud/enh/noid/user-creation-options

Opt-in for generation userid, requiring email addresses
This commit is contained in:
blizzz 2019-06-21 11:08:59 +02:00 committed by GitHub
commit c1eff72bdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 235 additions and 24 deletions

View File

@ -46,7 +46,6 @@ use OCP\IGroup;
use OCP\IGroupManager; use OCP\IGroupManager;
use OCP\ILogger; use OCP\ILogger;
use OCP\IRequest; use OCP\IRequest;
use OCP\IUser;
use OCP\IUserManager; use OCP\IUserManager;
use OCP\IUserSession; use OCP\IUserSession;
use OCP\L10N\IFactory; use OCP\L10N\IFactory;
@ -196,6 +195,21 @@ class UsersController extends AUserData {
]); ]);
} }
/**
* @throws OCSException
*/
private function createNewUserId(): string {
$attempts = 0;
do {
$uidCandidate = $this->secureRandom->generate(10, ISecureRandom::CHAR_HUMAN_READABLE);
if (!$this->userManager->userExists($uidCandidate)) {
return $uidCandidate;
}
$attempts++;
} while ($attempts < 10);
throw new OCSException('Could not create non-existing user id', 111);
}
/** /**
* @PasswordConfirmationRequired * @PasswordConfirmationRequired
* @NoAdminRequired * @NoAdminRequired
@ -223,6 +237,10 @@ class UsersController extends AUserData {
$isAdmin = $this->groupManager->isAdmin($user->getUID()); $isAdmin = $this->groupManager->isAdmin($user->getUID());
$subAdminManager = $this->groupManager->getSubAdmin(); $subAdminManager = $this->groupManager->getSubAdmin();
if(empty($userid) && $this->config->getAppValue('core', 'newUser.generateUserID', 'no') === 'yes') {
$userid = $this->createNewUserId();
}
if ($this->userManager->userExists($userid)) { if ($this->userManager->userExists($userid)) {
$this->logger->error('Failed addUser attempt: User already exists.', ['app' => 'ocs_api']); $this->logger->error('Failed addUser attempt: User already exists.', ['app' => 'ocs_api']);
throw new OCSException('User already exists', 102); throw new OCSException('User already exists', 102);
@ -275,6 +293,10 @@ class UsersController extends AUserData {
$generatePasswordResetToken = true; $generatePasswordResetToken = true;
} }
if ($email === '' && $this->config->getAppValue('core', 'newUser.requireEmail', 'no') === 'yes') {
throw new OCSException('Required email address was not provided', 110);
}
try { try {
$newUser = $this->userManager->createUser($userid, $password); $newUser = $this->userManager->createUser($userid, $password);
$this->logger->info('Successful addUser call with userid: ' . $userid, ['app' => 'ocs_api']); $this->logger->info('Successful addUser call with userid: ' . $userid, ['app' => 'ocs_api']);
@ -315,7 +337,7 @@ class UsersController extends AUserData {
} }
} }
return new DataResponse(); return new DataResponse(['UserID' => $userid]);
} catch (HintException $e ) { } catch (HintException $e ) {
$this->logger->logException($e, [ $this->logger->logException($e, [

View File

@ -356,7 +356,10 @@ class UsersControllerTest extends TestCase {
->with('adminUser') ->with('adminUser')
->willReturn(true); ->willReturn(true);
$this->assertEquals([], $this->api->addUser('NewUser', 'PasswordOfTheNewUser')->getData()); $this->assertTrue(key_exists(
'UserID',
$this->api->addUser('NewUser', 'PasswordOfTheNewUser')->getData()
));
} }
public function testAddUserSuccessfulWithDisplayName() { public function testAddUserSuccessfulWithDisplayName() {
@ -413,7 +416,149 @@ class UsersControllerTest extends TestCase {
->method('editUser') ->method('editUser')
->with('NewUser', 'display', 'DisplayNameOfTheNewUser'); ->with('NewUser', 'display', 'DisplayNameOfTheNewUser');
$this->assertEquals([], $api->addUser('NewUser', 'PasswordOfTheNewUser', 'DisplayNameOfTheNewUser')->getData()); $this->assertTrue(key_exists(
'UserID',
$api->addUser('NewUser', 'PasswordOfTheNewUser', 'DisplayNameOfTheNewUser')->getData()
));
}
public function testAddUserSuccessfulGenerateUserID() {
$this->config
->expects($this->any())
->method('getAppValue')
->willReturnCallback(function($appid, $key, $default) {
if($key === 'newUser.generateUserID') {
return 'yes';
}
return null;
});
$this->userManager
->expects($this->any())
->method('userExists')
->with($this->anything())
->will($this->returnValue(false));
$this->userManager
->expects($this->once())
->method('createUser')
->with($this->anything(), 'PasswordOfTheNewUser');
$this->logger
->expects($this->once())
->method('info')
->with($this->stringStartsWith('Successful addUser call with userid: '), ['app' => 'ocs_api']);
$loggedInUser = $this->getMockBuilder(IUser::class)
->disableOriginalConstructor()
->getMock();
$loggedInUser
->expects($this->once())
->method('getUID')
->will($this->returnValue('adminUser'));
$this->userSession
->expects($this->once())
->method('getUser')
->will($this->returnValue($loggedInUser));
$this->groupManager
->expects($this->once())
->method('isAdmin')
->with('adminUser')
->willReturn(true);
$this->secureRandom->expects($this->any())
->method('generate')
->with(10)
->willReturnCallback(function() { return (string)rand(1000000000, 9999999999); });
$this->assertTrue(key_exists(
'UserID',
$this->api->addUser('', 'PasswordOfTheNewUser')->getData()
));
}
/**
* @expectedException \OCP\AppFramework\OCS\OCSException
* @expectedExceptionCode 111
* @expectedExceptionMessage Could not create non-existing user id
*/
public function testAddUserFailedToGenerateUserID() {
$this->config
->expects($this->any())
->method('getAppValue')
->willReturnCallback(function($appid, $key, $default) {
if($key === 'newUser.generateUserID') {
return 'yes';
}
return null;
});
$this->userManager
->expects($this->any())
->method('userExists')
->with($this->anything())
->will($this->returnValue(true));
$this->userManager
->expects($this->never())
->method('createUser');
$loggedInUser = $this->getMockBuilder(IUser::class)
->disableOriginalConstructor()
->getMock();
$loggedInUser
->expects($this->once())
->method('getUID')
->will($this->returnValue('adminUser'));
$this->userSession
->expects($this->once())
->method('getUser')
->will($this->returnValue($loggedInUser));
$this->groupManager
->expects($this->once())
->method('isAdmin')
->with('adminUser')
->willReturn(true);
$this->api->addUser('', 'PasswordOfTheNewUser')->getData();
}
/**
* @expectedException \OCP\AppFramework\OCS\OCSException
* @expectedExceptionCode 110
* @expectedExceptionMessage Required email address was not provided
*/
public function testAddUserEmailRequired() {
$this->config
->expects($this->any())
->method('getAppValue')
->willReturnCallback(function($appid, $key, $default) {
if($key === 'newUser.requireEmail') {
return 'yes';
}
return null;
});
$this->userManager
->expects($this->once())
->method('userExists')
->with('NewUser')
->will($this->returnValue(false));
$this->userManager
->expects($this->never())
->method('createUser');
$loggedInUser = $this->getMockBuilder(IUser::class)
->disableOriginalConstructor()
->getMock();
$loggedInUser
->expects($this->once())
->method('getUID')
->will($this->returnValue('adminUser'));
$this->userSession
->expects($this->once())
->method('getUser')
->will($this->returnValue($loggedInUser));
$this->groupManager
->expects($this->once())
->method('isAdmin')
->with('adminUser')
->willReturn(true);
$this->assertTrue(key_exists(
'UserID',
$this->api->addUser('NewUser', 'PasswordOfTheNewUser')->getData()
));
} }
public function testAddUserExistingGroup() { public function testAddUserExistingGroup() {
@ -471,7 +616,10 @@ class UsersControllerTest extends TestCase {
['Added userid NewUser to group ExistingGroup', ['app' => 'ocs_api']] ['Added userid NewUser to group ExistingGroup', ['app' => 'ocs_api']]
); );
$this->assertEquals([], $this->api->addUser('NewUser', 'PasswordOfTheNewUser', '', '', ['ExistingGroup'])->getData()); $this->assertTrue(key_exists(
'UserID',
$this->api->addUser('NewUser', 'PasswordOfTheNewUser', '', '', ['ExistingGroup'])->getData()
));
} }
/** /**
@ -689,7 +837,10 @@ class UsersControllerTest extends TestCase {
) )
->willReturn(true); ->willReturn(true);
$this->assertEquals([], $this->api->addUser('NewUser', 'PasswordOfTheNewUser', '', '', ['ExistingGroup1', 'ExistingGroup2'])->getData()); $this->assertTrue(key_exists(
'UserID',
$this->api->addUser('NewUser', 'PasswordOfTheNewUser', '', '', ['ExistingGroup1', 'ExistingGroup2'])->getData()
));
} }
/** /**

View File

@ -635,7 +635,7 @@ class Access extends LDAPUtility {
return false; return false;
} }
protected function mapAndAnnounceIfApplicable( public function mapAndAnnounceIfApplicable(
AbstractMapping $mapper, AbstractMapping $mapper,
string $fdn, string $fdn,
string $name, string $name,

View File

@ -110,7 +110,7 @@ class Group_LDAP extends BackendUtility implements \OCP\GroupInterface, IGroupLD
$members = $this->access->connection->getFromCache($cacheKeyMembers); $members = $this->access->connection->getFromCache($cacheKeyMembers);
if(!is_null($members)) { if(!is_null($members)) {
$this->cachedGroupMembers[$gid] = $members; $this->cachedGroupMembers[$gid] = $members;
$isInGroup = in_array($userDN, $members); $isInGroup = in_array($userDN, $members, true);
$this->access->connection->writeToCache($cacheKey, $isInGroup); $this->access->connection->writeToCache($cacheKey, $isInGroup);
return $isInGroup; return $isInGroup;
} }

View File

@ -622,8 +622,26 @@ class User_LDAP extends BackendUtility implements \OCP\IUserBackend, \OCP\UserIn
if ($this->userPluginManager->implementsActions(Backend::CREATE_USER)) { if ($this->userPluginManager->implementsActions(Backend::CREATE_USER)) {
if ($dn = $this->userPluginManager->createUser($username, $password)) { if ($dn = $this->userPluginManager->createUser($username, $password)) {
if (is_string($dn)) { if (is_string($dn)) {
//updates user mapping // the NC user creation work flow requires a know user id up front
$this->access->dn2ocname($dn, $username, true); $uuid = $this->access->getUUID($dn, true);
if(is_string($uuid)) {
$this->access->mapAndAnnounceIfApplicable(
$this->access->getUserMapper(),
$dn,
$username,
$uuid,
true
);
$this->access->cacheUserExists($username);
} else {
\OC::$server->getLogger()->warning(
'Failed to map created LDAP user with userid {userid}, because UUID could not be determined',
[
'app' => 'user_ldap',
'userid' => $username,
]
);
}
} else { } else {
throw new \UnexpectedValueException("LDAP Plugin: Method createUser changed to return the user DN instead of boolean."); throw new \UnexpectedValueException("LDAP Plugin: Method createUser changed to return the user DN instead of boolean.");
} }

View File

@ -35,6 +35,7 @@ use OC\User\Backend;
use OC\User\Session; use OC\User\Session;
use OCA\User_LDAP\Access; use OCA\User_LDAP\Access;
use OCA\User_LDAP\Connection; use OCA\User_LDAP\Connection;
use OCA\User_LDAP\Mapping\AbstractMapping;
use OCA\User_LDAP\Mapping\UserMapping; use OCA\User_LDAP\Mapping\UserMapping;
use OCA\User_LDAP\User\Manager; use OCA\User_LDAP\User\Manager;
use OCA\User_LDAP\User\OfflineUser; use OCA\User_LDAP\User\OfflineUser;
@ -1437,16 +1438,30 @@ class User_LDAPTest extends TestCase {
} }
public function testCreateUserWithPlugin() { public function testCreateUserWithPlugin() {
$uid = 'alien6372';
$uuid = '123-2345-36756-123-2345234-4431';
$pwd = 'passwørd';
$this->pluginManager->expects($this->once()) $this->pluginManager->expects($this->once())
->method('implementsActions') ->method('implementsActions')
->with(Backend::CREATE_USER) ->with(Backend::CREATE_USER)
->willReturn(true); ->willReturn(true);
$this->pluginManager->expects($this->once()) $this->pluginManager->expects($this->once())
->method('createUser') ->method('createUser')
->with('uid','password') ->with($uid, $pwd)
->willReturn('result'); ->willReturn('result');
$this->assertEquals($this->backend->createUser('uid', 'password'),true); $this->access->expects($this->atLeastOnce())
->method('getUUID')
->willReturn($uuid);
$this->access->expects($this->once())
->method('mapAndAnnounceIfApplicable')
->with($this->isInstanceOf(AbstractMapping::class), $this->anything(), $uid, $uuid, true);
$this->access->expects($this->any())
->method('getUserMapper')
->willReturn($this->createMock(AbstractMapping::class));
$this->assertEquals($this->backend->createUser($uid, $pwd),true);
} }
public function testCreateUserFailing() { public function testCreateUserFailing() {

View File

@ -246,6 +246,8 @@ class UsersController extends Controller {
// Settings // Settings
$serverData['defaultQuota'] = $defaultQuota; $serverData['defaultQuota'] = $defaultQuota;
$serverData['canChangePassword'] = $canChangePassword; $serverData['canChangePassword'] = $canChangePassword;
$serverData['newUserGenerateUserID'] = $this->config->getAppValue('core', 'newUser.generateUserID', 'no') === 'yes';
$serverData['newUserRequireEmail'] = $this->config->getAppValue('core', 'newUser.requireEmail', 'no') === 'yes';
return new TemplateResponse('settings', 'settings-vue', ['serverData' => $serverData]); return new TemplateResponse('settings', 'settings-vue', ['serverData' => $serverData]);
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -49,9 +49,12 @@
<div :class="loading.all?'icon-loading-small':'icon-add'"></div> <div :class="loading.all?'icon-loading-small':'icon-add'"></div>
<div class="name"> <div class="name">
<input id="newusername" type="text" required v-model="newUser.id" <input id="newusername" type="text" required v-model="newUser.id"
:placeholder="t('settings', 'Username')" name="username" :placeholder="this.settings.newUserGenerateUserID
autocomplete="off" autocapitalize="none" autocorrect="off" ? t('settings', 'Will be autogenerated')
ref="newusername" pattern="[a-zA-Z0-9 _\.@\-']+"> : t('settings', 'Username')"
name="username" autocomplete="off" autocapitalize="none"
autocorrect="off" ref="newusername" pattern="[a-zA-Z0-9 _\.@\-']+"
:disabled="this.settings.newUserGenerateUserID">
</div> </div>
<div class="displayName"> <div class="displayName">
<input id="newdisplayname" type="text" v-model="newUser.displayName" <input id="newdisplayname" type="text" v-model="newUser.displayName"
@ -67,7 +70,7 @@
</div> </div>
<div class="mailAddress"> <div class="mailAddress">
<input id="newemail" type="email" v-model="newUser.mailAddress" <input id="newemail" type="email" v-model="newUser.mailAddress"
:required="newUser.password===''" :required="newUser.password==='' || this.settings.newUserRequireEmail"
:placeholder="t('settings', 'Email')" name="email" :placeholder="t('settings', 'Email')" name="email"
autocomplete="off" autocapitalize="none" autocorrect="off"> autocomplete="off" autocapitalize="none" autocorrect="off">
</div> </div>

View File

@ -429,7 +429,7 @@ const actions = {
addUser({commit, dispatch}, { userid, password, displayName, email, groups, subadmin, quota, language }) { addUser({commit, dispatch}, { userid, password, displayName, email, groups, subadmin, quota, language }) {
return api.requireAdmin().then((response) => { return api.requireAdmin().then((response) => {
return api.post(OC.linkToOCS(`cloud/users`, 2), { userid, password, displayName, email, groups, subadmin, quota, language }) return api.post(OC.linkToOCS(`cloud/users`, 2), { userid, password, displayName, email, groups, subadmin, quota, language })
.then((response) => dispatch('addUserData', userid)) .then((response) => dispatch('addUserData', userid || response.data.ocs.data.UserID))
.catch((error) => {throw error;}); .catch((error) => {throw error;});
}).catch((error) => { }).catch((error) => {
commit('API_FAILURE', { userid, error }); commit('API_FAILURE', { userid, error });