From 30f0a5e7f26211b9b30dbbe5363769efaf9c81fa Mon Sep 17 00:00:00 2001 From: Sujith H Date: Wed, 24 Jul 2019 16:35:38 +0530 Subject: [PATCH] Move user creation to a separate service Move user creation to a separate service. Signed-off-by: Sujith H --- apps/provisioning_api/appinfo/routes.php | 17 + apps/provisioning_api/lib/Users.php | 27 +- apps/provisioning_api/tests/UsersTest.php | 65 ++- core/Application.php | 7 +- core/Command/User/Add.php | 101 ++-- core/Controller/UserController.php | 205 +++++++- core/css/setpassword.css | 23 + core/js/setpassword.js | 83 +++ core/register_command.php | 16 +- core/routes.php | 3 + core/templates/new_user/email-html.php | 36 ++ core/templates/new_user/email-plain_text.php | 10 + core/templates/new_user/resendtokenbymail.php | 33 ++ core/templates/new_user/setpassword.php | 44 ++ core/templates/new_user/tokensendnotify.php | 2 + .../User/Service/CreateUserService.php | 202 ++++++++ .../User/Service/PasswordGeneratorService.php | 66 +++ .../User/Service/UserSendMailService.php | 190 +++++++ .../Exceptions/CannotCreateUserException.php | 31 ++ .../Exceptions/EmailSendFailedException.php | 31 ++ .../User/Exceptions/InvalidEmailException.php | 31 ++ .../Exceptions/InvalidUserTokenException.php | 42 ++ .../Exceptions/UserAlreadyExistsException.php | 31 ++ .../User/Exceptions/UserTokenException.php | 43 ++ .../Exceptions/UserTokenExpiredException.php | 42 ++ .../Exceptions/UserTokenMismatchException.php | 42 ++ tests/Core/Command/User/AddTest.php | 21 +- tests/Core/Controller/UserControllerTest.php | 477 ++++++++++++++++++ .../User/Service/CreateUserServiceTest.php | 173 +++++++ .../Service/PasswordGeneratorServiceTest.php | 62 +++ .../User/Service/UserSendMailServiceTest.php | 241 +++++++++ 31 files changed, 2294 insertions(+), 103 deletions(-) create mode 100644 core/css/setpassword.css create mode 100644 core/js/setpassword.js create mode 100644 core/templates/new_user/email-html.php create mode 100644 core/templates/new_user/email-plain_text.php create mode 100644 core/templates/new_user/resendtokenbymail.php create mode 100644 core/templates/new_user/setpassword.php create mode 100644 core/templates/new_user/tokensendnotify.php create mode 100644 lib/private/User/Service/CreateUserService.php create mode 100644 lib/private/User/Service/PasswordGeneratorService.php create mode 100644 lib/private/User/Service/UserSendMailService.php create mode 100644 lib/public/User/Exceptions/CannotCreateUserException.php create mode 100644 lib/public/User/Exceptions/EmailSendFailedException.php create mode 100644 lib/public/User/Exceptions/InvalidEmailException.php create mode 100644 lib/public/User/Exceptions/InvalidUserTokenException.php create mode 100644 lib/public/User/Exceptions/UserAlreadyExistsException.php create mode 100644 lib/public/User/Exceptions/UserTokenException.php create mode 100644 lib/public/User/Exceptions/UserTokenExpiredException.php create mode 100644 lib/public/User/Exceptions/UserTokenMismatchException.php create mode 100644 tests/Core/Controller/UserControllerTest.php create mode 100644 tests/lib/User/Service/CreateUserServiceTest.php create mode 100644 tests/lib/User/Service/PasswordGeneratorServiceTest.php create mode 100644 tests/lib/User/Service/UserSendMailServiceTest.php diff --git a/apps/provisioning_api/appinfo/routes.php b/apps/provisioning_api/appinfo/routes.php index 78cbddc0533c..f2d0b25f7704 100644 --- a/apps/provisioning_api/appinfo/routes.php +++ b/apps/provisioning_api/appinfo/routes.php @@ -27,6 +27,9 @@ namespace OCA\Provisioning_API\AppInfo; +use OC\User\Service\CreateUserService; +use OC\User\Service\PasswordGeneratorService; +use OC\User\Service\UserSendMailService; use OCA\Provisioning_API\Apps; use OCA\Provisioning_API\Groups; use OCA\Provisioning_API\Users; @@ -37,6 +40,20 @@ \OC::$server->getUserManager(), \OC::$server->getGroupManager(), \OC::$server->getUserSession(), + new CreateUserService( + \OC::$server->getUserSession(), \OC::$server->getGroupManager(), + \OC::$server->getUserManager(), \OC::$server->getSecureRandom(), + \OC::$server->getLogger(), + new UserSendMailService( + \OC::$server->getSecureRandom(), \OC::$server->getConfig(), + \OC::$server->getMailer(), \OC::$server->getURLGenerator(), + new \OC_Defaults(), \OC::$server->getTimeFactory(), + \OC::$server->getL10N('settings') + ), + new PasswordGeneratorService( + \OC::$server->getEventDispatcher(), \OC::$server->getSecureRandom() + ) + ), \OC::$server->getLogger(), \OC::$server->getTwoFactorAuthManager() ); diff --git a/apps/provisioning_api/lib/Users.php b/apps/provisioning_api/lib/Users.php index 06392cf38402..b3b656b6f365 100644 --- a/apps/provisioning_api/lib/Users.php +++ b/apps/provisioning_api/lib/Users.php @@ -30,6 +30,7 @@ namespace OCA\Provisioning_API; use OC\OCS\Result; +use OC\User\Service\CreateUserService; use OC_Helper; use OCP\API; use OCP\Files\FileInfo; @@ -50,25 +51,33 @@ class Users { private $groupManager; /** @var IUserSession */ private $userSession; + /** @var CreateUserService */ + private $createUserService; /** @var ILogger */ private $logger; /** @var \OC\Authentication\TwoFactorAuth\Manager */ private $twoFactorAuthManager; /** + * Users constructor. + * * @param IUserManager $userManager * @param IGroupManager $groupManager * @param IUserSession $userSession + * @param CreateUserService $createUserService * @param ILogger $logger + * @param \OC\Authentication\TwoFactorAuth\Manager $twoFactorAuthManager */ public function __construct(IUserManager $userManager, IGroupManager $groupManager, IUserSession $userSession, + CreateUserService $createUserService, ILogger $logger, \OC\Authentication\TwoFactorAuth\Manager $twoFactorAuthManager) { $this->userManager = $userManager; $this->groupManager = $groupManager; $this->userSession = $userSession; + $this->createUserService = $createUserService; $this->logger = $logger; $this->twoFactorAuthManager = $twoFactorAuthManager; } @@ -125,8 +134,9 @@ public function getUsers() { */ public function addUser() { $userId = isset($_POST['userid']) ? $_POST['userid'] : null; - $password = isset($_POST['password']) ? $_POST['password'] : null; - $groups = isset($_POST['groups']) ? $_POST['groups'] : null; + $password = isset($_POST['password']) ? $_POST['password'] : ''; + $groups = isset($_POST['groups']) ? $_POST['groups'] : []; + $emailAddress = isset($_POST['email']) ? $_POST['email'] : ''; $user = $this->userSession->getUser(); $isAdmin = $this->groupManager->isAdmin($user->getUID()); $subAdminManager = $this->groupManager->getSubAdmin(); @@ -140,7 +150,7 @@ public function addUser() { return new Result(null, 102, 'User already exists'); } - if (\is_array($groups)) { + if (\is_array($groups) && (\count($groups) > 0)) { foreach ($groups as $group) { if (!$this->groupManager->groupExists($group)) { return new Result(null, 104, 'group '.$group.' does not exist'); @@ -156,13 +166,14 @@ public function addUser() { } try { - $newUser = $this->userManager->createUser($userId, $password); + $newUser = $this->createUserService->createUser(['username' => $userId, 'password' => $password, 'email' => $emailAddress]); $this->logger->info('Successful addUser call with userid: '.$userId, ['app' => 'ocs_api']); - if (\is_array($groups)) { - foreach ($groups as $group) { - $this->groupManager->get($group)->addUser($newUser); - $this->logger->info('Added userid '.$userId.' to group '.$group, ['app' => 'ocs_api']); + if (\count($groups) > 0) { + $failedToAddGroups = $this->createUserService->addUserToGroups($newUser, $groups); + if (\count($failedToAddGroups) > 0) { + $failedToAddGroups = \implode(',', $failedToAddGroups); + $this->logger->error("User $userId could not be added to groups $failedToAddGroups"); } } return new Result(null, 100); diff --git a/apps/provisioning_api/tests/UsersTest.php b/apps/provisioning_api/tests/UsersTest.php index 0c1b7503faf3..409c263ba52b 100644 --- a/apps/provisioning_api/tests/UsersTest.php +++ b/apps/provisioning_api/tests/UsersTest.php @@ -30,6 +30,7 @@ namespace OCA\Provisioning_API\Tests; use OC\OCS\Result; +use OC\User\Service\CreateUserService; use OCA\Provisioning_API\Users; use OCP\API; use OCP\ILogger; @@ -56,6 +57,8 @@ class UsersTest extends OriginalTest { protected $api; /** @var \OC\Authentication\TwoFactorAuth\Manager | PHPUnit\Framework\MockObject\MockObject */ private $twoFactorAuthManager; + /** @var CreateUserService | PHPUnit\Framework\MockObject\MockObject */ + private $createUserService; protected function tearDown() { $_GET = null; @@ -79,11 +82,13 @@ protected function setUp() { $this->twoFactorAuthManager->expects($this->any()) ->method('isTwoFactorAuthenticated') ->willReturn(false); + $this->createUserService = $this->createMock(CreateUserService::class); $this->api = $this->getMockBuilder(Users::class) ->setConstructorArgs([ $this->userManager, $this->groupManager, $this->userSession, + $this->createUserService, $this->logger, $this->twoFactorAuthManager ]) @@ -324,15 +329,17 @@ public function testAddUserExistingGroupNonExistingGroup() { public function testAddUserSuccessful() { $_POST['userid'] = 'NewUser'; $_POST['password'] = 'PasswordOfTheNewUser'; + $newUserCreated = $this->createMock(IUser::class); $this->userManager ->expects($this->once()) ->method('userExists') ->with('NewUser') ->will($this->returnValue(false)); - $this->userManager + $this->createUserService ->expects($this->once()) ->method('createUser') - ->with('NewUser', 'PasswordOfTheNewUser'); + ->with(['username' => 'NewUser', 'password' => 'PasswordOfTheNewUser', 'email' => '']) + ->will($this->returnValue($newUserCreated)); $this->logger ->expects($this->once()) ->method('info') @@ -385,27 +392,22 @@ public function testAddUserExistingGroup() { ->with('ExistingGroup') ->willReturn(true); $user = $this->createMock(IUser::class); - $this->userManager + $this->createUserService ->expects($this->once()) ->method('createUser') - ->with('NewUser', 'PasswordOfTheNewUser') + ->with(['username' => 'NewUser', 'password' => 'PasswordOfTheNewUser', 'email' => '']) ->willReturn($user); - $group = $this->createMock(IGroup::class); - $group - ->expects($this->once()) - ->method('addUser') - ->with($user); - $this->groupManager + $this->createUserService ->expects($this->once()) - ->method('get') - ->with('ExistingGroup') - ->willReturn($group); + ->method('addUserToGroups') + ->with($user, ['ExistingGroup']) + ->will($this->returnValue([])); + $group = $this->createMock(IGroup::class); $this->logger - ->expects($this->exactly(2)) + ->expects($this->exactly(1)) ->method('info') ->withConsecutive( - ['Successful addUser call with userid: NewUser', ['app' => 'ocs_api']], - ['Added userid NewUser to group ExistingGroup', ['app' => 'ocs_api']] + ['Successful addUser call with userid: NewUser', ['app' => 'ocs_api']] ); $expected = new Result(null, 100); @@ -420,10 +422,10 @@ public function testAddUserUnsuccessful() { ->method('userExists') ->with('NewUser') ->will($this->returnValue(false)); - $this->userManager + $this->createUserService ->expects($this->once()) ->method('createUser') - ->with('NewUser', 'PasswordOfTheNewUser') + ->with(['username' => 'NewUser', 'password' => 'PasswordOfTheNewUser', 'email' => '']) ->will($this->throwException(new \Exception('User backend not found.'))); $this->logger ->expects($this->once()) @@ -599,27 +601,22 @@ public function testAddUserAsSubAdminExistingGroups() { ) ->willReturn(true); $user = $this->createMock(IUser::class); - $this->userManager + $this->createUserService ->expects($this->once()) ->method('createUser') - ->with('NewUser', 'PasswordOfTheNewUser') + ->with(['username' => 'NewUser', 'password' => 'PasswordOfTheNewUser', 'email' => '']) ->willReturn($user); + $this->createUserService + ->expects($this->once()) + ->method('addUserToGroups') + ->with($user, ['ExistingGroup1', 'ExistingGroup2']) + ->willReturn([]); $existingGroup1 = $this->createMock(IGroup::class); $existingGroup2 = $this->createMock(IGroup::class); - $existingGroup1 - ->expects($this->once()) - ->method('addUser') - ->with($user); - $existingGroup2 - ->expects($this->once()) - ->method('addUser') - ->with($user); $this->groupManager - ->expects($this->exactly(4)) + ->expects($this->exactly(2)) ->method('get') ->withConsecutive( - ['ExistingGroup1'], - ['ExistingGroup2'], ['ExistingGroup1'], ['ExistingGroup2'] ) @@ -628,12 +625,10 @@ public function testAddUserAsSubAdminExistingGroups() { ['ExistingGroup2', $existingGroup2] ])); $this->logger - ->expects($this->exactly(3)) + ->expects($this->exactly(1)) ->method('info') ->withConsecutive( - ['Successful addUser call with userid: NewUser', ['app' => 'ocs_api']], - ['Added userid NewUser to group ExistingGroup1', ['app' => 'ocs_api']], - ['Added userid NewUser to group ExistingGroup2', ['app' => 'ocs_api']] + ['Successful addUser call with userid: NewUser', ['app' => 'ocs_api']] ); $subAdminManager = $this->getMockBuilder(SubAdmin::class) ->disableOriginalConstructor()->getMock(); diff --git a/core/Application.php b/core/Application.php index f98b7e92286b..a6b815f7ecb4 100644 --- a/core/Application.php +++ b/core/Application.php @@ -44,6 +44,7 @@ use OCP\IConfig; use OCP\ILogger; use OCP\IServerContainer; +use OCP\IURLGenerator; use OCP\Util; /** @@ -87,7 +88,11 @@ public function __construct(array $urlParams= []) { $c->query('AppName'), $c->query('Request'), $c->query('UserManager'), - $c->query('Defaults') + $c->query('Defaults'), + $c->query('OC\User\Service\UserSendMailService'), + $c->query('URLGenerator'), + $c->query('Logger'), + $c->query('L10N') ); }); $container->registerService('AvatarController', function (SimpleContainer $c) { diff --git a/core/Command/User/Add.php b/core/Command/User/Add.php index 675bbe2ddb9e..d70019618bf5 100644 --- a/core/Command/User/Add.php +++ b/core/Command/User/Add.php @@ -23,11 +23,12 @@ namespace OC\Core\Command\User; -use OC\Files\Filesystem; +use OC\User\Service\CreateUserService; use OCP\IGroupManager; use OCP\IUser; -use OCP\IUserManager; -use OCP\Mail\IMailer; +use OCP\User\Exceptions\CannotCreateUserException; +use OCP\User\Exceptions\InvalidEmailException; +use OCP\User\Exceptions\UserAlreadyExistsException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -36,25 +37,23 @@ use Symfony\Component\Console\Question\Question; class Add extends Command { - /** @var \OCP\IUserManager */ - protected $userManager; - /** @var \OCP\IGroupManager */ protected $groupManager; - /** @var IMailer */ - protected $mailer; + /** @var CreateUserService */ + private $createUserService; /** - * @param IUserManager $userManager + * Add constructor. + * * @param IGroupManager $groupManager - * @param IMailer $mailer + * @param CreateUserService $createUserService */ - public function __construct(IUserManager $userManager, IGroupManager $groupManager, IMailer $mailer) { + public function __construct(IGroupManager $groupManager, + CreateUserService $createUserService) { parent::__construct(); - $this->userManager = $userManager; $this->groupManager = $groupManager; - $this->mailer = $mailer; + $this->createUserService = $createUserService; } protected function configure() { @@ -94,32 +93,28 @@ protected function configure() { protected function execute(InputInterface $input, OutputInterface $output) { $uid = $input->getArgument('uid'); - if ($this->userManager->userExists($uid)) { + $email = $input->getOption('email'); + $displayName = $input->getOption('display-name'); + $passwordFromEnv = $input->getOption('password-from-env'); + $groupInput = $input->getOption('group'); + + if ($this->createUserService->userExists($uid)) { $output->writeln('The user "' . $uid . '" already exists.'); return 1; } - // Validate email before we create the user - if ($input->getOption('email')) { - // Validate first - if (!$this->mailer->validateMailAddress($input->getOption('email'))) { - // Invalid! Error - $output->writeln('Invalid email address supplied'); - return 1; - } else { - $email = $input->getOption('email'); - } - } else { - $email = null; + if (!$email) { + $email = ''; } - if ($input->getOption('password-from-env')) { + $password = ''; + if ($passwordFromEnv) { $password = \getenv('OC_PASS'); if (!$password) { $output->writeln('--password-from-env given, but OC_PASS is empty!'); return 1; } - } elseif ($input->isInteractive()) { + } elseif (($email === '') && $input->isInteractive()) { /** @var $dialog \Symfony\Component\Console\Helper\QuestionHelper */ $dialog = $this->getHelperSet()->get('question'); $q = new Question('Enter password: ', false); @@ -133,15 +128,20 @@ protected function execute(InputInterface $input, OutputInterface $output) { $output->writeln("Passwords did not match!"); return 1; } - } else { - $output->writeln("Interactive input or --password-from-env is needed for entering a password!"); - return 1; } - $user = $this->userManager->createUser( - $input->getArgument('uid'), - $password - ); + try { + $user = $this->createUserService->createUser(['username' => $uid, 'password' => $password, 'email' => $email]); + } catch (InvalidEmailException $e) { + $output->writeln('Invalid email address supplied'); + return 1; + } catch (CannotCreateUserException $e) { + $output->writeln("" . $e->getMessage() . ""); + return 1; + } catch (UserAlreadyExistsException $e) { + $output->writeln("" . $e->getMessage() . ""); + return 1; + } if ($user instanceof IUser) { $output->writeln('The user "' . $user->getUID() . '" was created successfully'); @@ -150,34 +150,25 @@ protected function execute(InputInterface $input, OutputInterface $output) { return 1; } - if ($input->getOption('display-name')) { - $user->setDisplayName($input->getOption('display-name')); + if ($displayName) { + $user->setDisplayName($displayName); $output->writeln('Display name set to "' . $user->getDisplayName() . '"'); } // Set email if supplied & valid - if ($email !== null) { - $user->setEMailAddress($email); + if ($email !== '') { $output->writeln('Email address set to "' . $user->getEMailAddress() . '"'); } - $groups = $input->getOption('group'); - - if (!empty($groups)) { - // Make sure we init the Filesystem for the user, in case we need to - // init some group shares. - Filesystem::init($user->getUID(), ''); + $failedToAddGroups = $this->createUserService->addUserToGroups($user, $groupInput); + if (\count($failedToAddGroups) > 0) { + $failedGroups = \implode(',', $failedToAddGroups); + $output->writeln("Unable to add user: $uid to groups: $failedGroups"); + return 2; } - - foreach ($groups as $groupName) { - $group = $this->groupManager->get($groupName); - if (!$group) { - $this->groupManager->createGroup($groupName); - $group = $this->groupManager->get($groupName); - $output->writeln('Created group "' . $group->getGID() . '"'); - } - $group->addUser($user); - $output->writeln('User "' . $user->getUID() . '" added to group "' . $group->getGID() . '"'); + foreach ($groupInput as $groupName) { + $output->writeln("User $uid added to group $groupName"); } + return 0; } } diff --git a/core/Controller/UserController.php b/core/Controller/UserController.php index 663a7e7e03b1..12f1a13bcd3b 100644 --- a/core/Controller/UserController.php +++ b/core/Controller/UserController.php @@ -3,6 +3,7 @@ * @author Lukas Reschke * @author Morris Jobke * @author Thomas Müller + * @author Sujith Haridasan * * @copyright Copyright (c) 2018, ownCloud GmbH * @license AGPL-3.0 @@ -23,9 +24,18 @@ namespace OC\Core\Controller; +use OC\User\Service\UserSendMailService; use \OCP\AppFramework\Controller; +use OCP\AppFramework\Http; use \OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IL10N; +use OCP\ILogger; use \OCP\IRequest; +use OCP\IURLGenerator; +use OCP\User\Exceptions\EmailSendFailedException; +use OCP\User\Exceptions\UserTokenException; +use OCP\User\Exceptions\UserTokenExpiredException; class UserController extends Controller { /** @@ -38,14 +48,45 @@ class UserController extends Controller { */ protected $defaults; + /** @var UserSendMailService */ + private $userSendMailService; + + /** @var IURLGenerator */ + private $urlGenerator; + + /** @var ILogger */ + private $logger; + + /** @var IL10N */ + private $l10n; + + /** + * UserController constructor. + * + * @param $appName + * @param IRequest $request + * @param $userManager + * @param $defaults + * @param UserSendMailService $userSendMailService + * @param IURLGenerator $urlGenerator + * @param ILogger $logger + * @param IL10N $l10n + */ public function __construct($appName, IRequest $request, $userManager, - $defaults + $defaults, + UserSendMailService $userSendMailService, + IURLGenerator $urlGenerator, ILogger $logger, + IL10N $l10n ) { parent::__construct($appName, $request); $this->userManager = $userManager; $this->defaults = $defaults; + $this->userSendMailService = $userSendMailService; + $this->urlGenerator = $urlGenerator; + $this->logger = $logger; + $this->l10n = $l10n; } /** @@ -76,4 +117,166 @@ public function getDisplayNames($users) { return new JSONResponse($json); } + + /** + * Set password for user using link + * + * @PublicPage + * @NoCSRFRequired + * @NoAdminRequired + * @NoSubadminRequired + * + * @param string $token + * @param string $userId + * @return TemplateResponse + */ + public function setPasswordForm($token, $userId) { + try { + $user = $this->userManager->get($userId); + $this->userSendMailService->checkPasswordSetToken($token, $user); + } catch (UserTokenException $e) { + if ($e instanceof UserTokenExpiredException) { + return new TemplateResponse( + 'core', 'new_user/resendtokenbymail', + [ + 'link' => $this->urlGenerator->linkToRouteAbsolute('core.user.resendToken', ['userId' => $userId]) + ], 'guest' + ); + } + $this->logger->logException($e, ['app' => 'core']); + return new TemplateResponse( + 'core', 'error', + [ + "errors" => [["error" => $this->l10n->t($e->getMessage())]] + ], 'guest' + ); + } + + return new TemplateResponse( + 'core', 'new_user/setpassword', + [ + 'link' => $this->urlGenerator->linkToRouteAbsolute('core.user.setPassword', ['userId' => $userId, 'token' => $token]) + ], 'guest' + ); + } + + /** + * @PublicPage + * @NoCSRFRequired + * @NoAdminRequired + * @NoSubadminRequired + * + * @param string $userId + * @return TemplateResponse + */ + public function resendToken($userId) { + $user = $this->userManager->get($userId); + + if ($user === null) { + $this->logger->error('User: ' . $userId . ' does not exist', ['app' => 'core']); + return new TemplateResponse( + 'core', 'error', + [ + "errors" => [["error" => $this->l10n->t('Failed to create activation link. Please contact your administrator.')]] + ], + 'guest' + ); + } + + if ($user->getEMailAddress() === null) { + $this->logger->error('Email address not set for: ' . $userId, ['app' => 'core']); + return new TemplateResponse( + 'core', 'error', + [ + "errors" => [["error" => $this->l10n->t('Failed to create activation link. Please contact your administrator.', [$userId])]] + ], + 'guest' + ); + } + + try { + $this->userSendMailService->generateTokenAndSendMail($user->getUID(), $user->getEMailAddress()); + } catch (\Exception $e) { + $this->logger->error("Can't send new user mail to " . $user->getEMailAddress() . ": " . $e->getMessage(), ['app' => 'core']); + return new TemplateResponse( + 'core', 'error', + [ + "errors" => [[ + "error" => $this->l10n->t('Can\'t send email to the user. Contact your administrator.')]] + ], 'guest' + ); + } + + return new TemplateResponse( + 'core', 'new_user/tokensendnotify', [], 'guest' + ); + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoSubadminRequired + * @NoCSRFRequired + * + * @param string $token + * @param string $userId + * @param string $password + * @return JSONResponse + */ + public function setPassword($token, $userId, $password) { + $user = $this->userManager->get($userId); + + if ($user === null) { + $this->logger->error('User: ' . $userId . ' does not exist.', ['app' => 'core']); + return new JSONResponse( + [ + 'status' => 'error', + 'message' => $this->l10n->t('Failed to set password. Please contact the administrator.', [$userId]), + 'type' => 'usererror' + ], Http::STATUS_NOT_FOUND + ); + } + + try { + $this->userSendMailService->checkPasswordSetToken($token, $user); + + if (!$user->setPassword($password)) { + $this->logger->error('The password can not be set for user: '. $userId); + return new JSONResponse( + [ + 'status' => 'error', + 'message' => $this->l10n->t('Failed to set password. Please contact your administrator.', [$userId]), + 'type' => 'passwordsetfailed' + ], Http::STATUS_FORBIDDEN + ); + } + + \OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'post_passwordReset', ['uid' => $userId, 'password' => $password]); + @\OC_User::unsetMagicInCookie(); + } catch (UserTokenException $e) { + $this->logger->logException($e, ['app' => 'core']); + return new JSONResponse( + [ + 'status' => 'error', + 'message' => $e->getMessage(), + 'type' => 'tokenfailure' + ], Http::STATUS_UNAUTHORIZED + ); + } + + try { + $this->userSendMailService->sendNotificationMail($user); + } catch (EmailSendFailedException $e) { + $this->logger->logException($e, ['app' => 'user_management']); + return new JSONResponse( + [ + 'status' => 'error', + 'message' => $this->l10n->t('Failed to send email. Please contact your administrator.'), + 'type' => 'emailsendfailed' + ], Http::STATUS_INTERNAL_SERVER_ERROR + ); + } + + return new JSONResponse(['status' => 'success']); + } } diff --git a/core/css/setpassword.css b/core/css/setpassword.css new file mode 100644 index 000000000000..69c1342b569c --- /dev/null +++ b/core/css/setpassword.css @@ -0,0 +1,23 @@ +#reset-password p { + position: relative; +} + +.text-center { + text-align: center; +} + +#submit { + width: 100%; +} + +#password { + width: 100%; +} + +#retypepassword { + width: 100%; +} + +#message { + width: 94%; +} diff --git a/core/js/setpassword.js b/core/js/setpassword.js new file mode 100644 index 000000000000..35df031ebc3e --- /dev/null +++ b/core/js/setpassword.js @@ -0,0 +1,83 @@ +(function () { + var SetPassword = { + init : function() { + $('#set-password #submit').click(this.onClickSetPassword); + }, + + onClickSetPassword : function(event){ + var passwordObj = $('#password'); + var retypePasswordObj = $('#retypepassword'); + passwordObj.parent().removeClass('shake'); + event.preventDefault(); + if (passwordObj.val() === retypePasswordObj.val()) { + $.post( + passwordObj.parents('form').attr('action'), + {password: passwordObj.val()} + ).done(function (result) { + OC.User.SetPassword._resetDone(result); + }).fail(function (result) { + OC.User.SetPassword._onSetPasswordFail(result); + }); + } else { + //Password mismatch happened + passwordObj.val(''); + retypePasswordObj.val(''); + passwordObj.parent().addClass('shake'); + $('#message').addClass('warning'); + $('#message').text('Passwords do not match'); + $('#message').show(); + passwordObj.focus(); + } + }, + + _onSetPasswordFail: function(result) { + var responseObj = JSON.parse(result.responseText); + var errorObject = $('#error-message'); + var showErrorMessage = false; + + var errorMessage; + errorMessage = responseObj.message; + + if (!errorMessage) { + errorMessage = t('core', 'Failed to set password. Please contact your administrator.'); + } + + errorObject.text(errorMessage); + errorObject.show(); + $('#submit').prop('disabled', true); + }, + + _resetDone : function(result){ + if (result && result.status === 'success') { + var getRootPath = OC.getRootPath(); + if (getRootPath === '') { + /** + * If owncloud is not run inside subfolder, the getRootPath + * will return empty string + */ + getRootPath = "/"; + } + OC.redirect(getRootPath); + } + } + }; + + if (!OC.User) { + OC.User = {}; + } + OC.User.SetPassword = SetPassword; +})(); + +$(document).ready(function () { + OC.User.SetPassword.init(); + $('#password').keypress(function () { + /* + The warning message should be shown only during password mismatch. + Else it should not. + */ + if (($('#password').val().length >= 0) && ($('#retypepassword').val().length === 0)) { + $('#message').removeClass('warning'); + $('#message').text(''); + } + }); +}); diff --git a/core/register_command.php b/core/register_command.php index c4ed1d9341b2..97c7ba9cf006 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -146,7 +146,21 @@ \OC::$server->getAppManager()) ); - $application->add(new OC\Core\Command\User\Add(\OC::$server->getUserManager(), \OC::$server->getGroupManager(), \OC::$server->getMailer())); + $application->add(new OC\Core\Command\User\Add(\OC::$server->getGroupManager(), + new \OC\User\Service\CreateUserService( + \OC::$server->getUserSession(), \OC::$server->getGroupManager(), + \OC::$server->getUserManager(), \OC::$server->getSecureRandom(), + \OC::$server->getLogger(), + new \OC\User\Service\UserSendMailService( + \OC::$server->getSecureRandom(), \OC::$server->getConfig(), + OC::$server->getMailer(), \OC::$server->getURLGenerator(), + new \OC_Defaults(), \OC::$server->getTimeFactory(), + \OC::$server->getL10N('settings') + ), + new \OC\User\Service\PasswordGeneratorService(\OC::$server->getEventDispatcher(), + \OC::$server->getSecureRandom()) + ) + )); $application->add(new OC\Core\Command\User\Delete(\OC::$server->getUserManager())); $application->add(new OC\Core\Command\User\Disable(\OC::$server->getUserManager())); $application->add(new OC\Core\Command\User\Enable(\OC::$server->getUserManager())); diff --git a/core/routes.php b/core/routes.php index 97e109a8c69b..c9bf73e53f69 100644 --- a/core/routes.php +++ b/core/routes.php @@ -38,6 +38,9 @@ ['name' => 'lost#resetform', 'url' => '/lostpassword/reset/form/{token}/{userId}', 'verb' => 'GET'], ['name' => 'lost#setPassword', 'url' => '/lostpassword/set/{token}/{userId}', 'verb' => 'POST'], ['name' => 'user#getDisplayNames', 'url' => '/displaynames', 'verb' => 'POST'], + ['name' => 'user#setPassword', 'url' => '/setpassword/{token}/{userId}', 'verb' => 'POST'], + ['name' => 'user#resendToken', 'url' => '/resend/token/{userId}', 'verb' => 'POST'], + ['name' => 'user#setPasswordForm', 'url' => '/setpassword/form/{token}/{userId}', 'verb' => 'GET'], ['name' => 'avatar#getAvatar', 'url' => '/avatar/{userId}/{size}', 'verb' => 'GET'], ['name' => 'avatar#deleteAvatar', 'url' => '/avatar/', 'verb' => 'DELETE'], ['name' => 'avatar#postCroppedAvatar', 'url' => '/avatar/cropped', 'verb' => 'POST'], diff --git a/core/templates/new_user/email-html.php b/core/templates/new_user/email-html.php new file mode 100644 index 000000000000..716755df22a6 --- /dev/null +++ b/core/templates/new_user/email-html.php @@ -0,0 +1,36 @@ + + +
+ + + + + + + + + + + + + + + + + + +
  + <?php p($theme->getName()); ?> +
 
  + t('Hey there,

just letting you know that you now have an %s account.

Your username: %s
Please set the password by accessing it: Here

', [$theme->getName(), $_['username'], $_['url']])); + + // TRANSLATORS term at the end of a mail + p($l->t('Cheers!')); + ?> +
 
 --
+ getName()); ?> - + getSlogan()); ?> +
getBaseUrl());?> +
 
+
diff --git a/core/templates/new_user/email-plain_text.php b/core/templates/new_user/email-plain_text.php new file mode 100644 index 000000000000..21daeed4c26f --- /dev/null +++ b/core/templates/new_user/email-plain_text.php @@ -0,0 +1,10 @@ +t("Hey there,\n\njust letting you know that you now have an %s account.\n\nYour username: %s\nAccess it: %s\n\n", [$theme->getName(), $_['username'], $_['url']])); + +// TRANSLATORS term at the end of a mail +p($l->t("Cheers!")); +?> + + -- +getName() . ' - ' . $theme->getSlogan()); ?> +getBaseUrl()); diff --git a/core/templates/new_user/resendtokenbymail.php b/core/templates/new_user/resendtokenbymail.php new file mode 100644 index 000000000000..0c6bedbcf065 --- /dev/null +++ b/core/templates/new_user/resendtokenbymail.php @@ -0,0 +1,33 @@ + + * + * @copyright Copyright (c) 2018, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +?> + +
+
+

+ +

+ +
+
diff --git a/core/templates/new_user/setpassword.php b/core/templates/new_user/setpassword.php new file mode 100644 index 000000000000..3eb2ba9cf347 --- /dev/null +++ b/core/templates/new_user/setpassword.php @@ -0,0 +1,44 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +style('core', 'setpassword'); +script('core', 'setpassword'); +?> + + +
+
+

+ + + + +

+ +
+
diff --git a/core/templates/new_user/tokensendnotify.php b/core/templates/new_user/tokensendnotify.php new file mode 100644 index 000000000000..13176743b951 --- /dev/null +++ b/core/templates/new_user/tokensendnotify.php @@ -0,0 +1,2 @@ +t('Activation link was sent to an email address, if one was configured.')); diff --git a/lib/private/User/Service/CreateUserService.php b/lib/private/User/Service/CreateUserService.php new file mode 100644 index 000000000000..918edbe084bb --- /dev/null +++ b/lib/private/User/Service/CreateUserService.php @@ -0,0 +1,202 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OC\User\Service; + +use OCP\IGroupManager; +use OCP\ILogger; +use OCP\IUser; +use OCP\IUserManager; +use OCP\IUserSession; +use OCP\Security\ISecureRandom; +use OCP\User\Exceptions\CannotCreateUserException; +use OCP\User\Exceptions\InvalidEmailException; +use OCP\User\Exceptions\UserAlreadyExistsException; + +class CreateUserService { + /** @var IUserSession */ + private $userSession; + /** @var IGroupManager */ + private $groupManager; + /** @var IUserManager */ + private $userManager; + /** @var ISecureRandom */ + private $secureRandom; + /** @var ILogger */ + private $logger; + /** @var UserSendMailService */ + private $userSendMailService; + /** @var PasswordGeneratorService */ + private $passwordGeneratorService; + + /** + * CreateUserService constructor. + * + * @param IUserSession $userSession + * @param IGroupManager $groupManager + * @param IUserManager $userManager + * @param ISecureRandom $secureRandom + * @param ILogger $logger + * @param UserSendMailService $userSendMailService + * @param PasswordGeneratorService $passwordGeneratorService + */ + public function __construct(IUserSession $userSession, IGroupManager $groupManager, + IUserManager $userManager, ISecureRandom $secureRandom, + ILogger $logger, UserSendMailService $userSendMailService, + PasswordGeneratorService $passwordGeneratorService) { + $this->userSession = $userSession; + $this->groupManager = $groupManager; + $this->userManager = $userManager; + $this->secureRandom = $secureRandom; + $this->logger = $logger; + $this->userSendMailService = $userSendMailService; + $this->passwordGeneratorService = $passwordGeneratorService; + } + + /** + * Create a new user + * + * The function accepts array of arguments. The arguments could be one of the following: + * - ['username' => 'myuser'] - will not be allowed, cause exception from usermanager createUser + * - ['username' => 'myuser', 'password' => 'foo'] - will create a user + * - ['username' => 'myuser', 'email' => 'hello@example.com'] - will create user and send email to the new user + * - ['username' => 'myuser', 'password' => 'foo', 'email => 'hello@example.com'] - will create new user and set the email address to new user. + * + * The optional arguments in the array are: + * - email + * - password + * Do not skip both email and password, it will not create the new user. + * + * @param array $arguments + * @return IUser + * @throws CannotCreateUserException + * @throws InvalidEmailException + * @throws UserAlreadyExistsException + */ + public function createUser($arguments) { + $username = $password = $email = ''; + if (\array_key_exists('username', $arguments)) { + $username = $arguments['username']; + } else { + throw new CannotCreateUserException("Unable to create user due to missing user name"); + } + if (\array_key_exists('password', $arguments)) { + $password = $arguments['password']; + } + if (\array_key_exists('email', $arguments)) { + $email = $arguments['email']; + } + + if ($email !== '' && !$this->userSendMailService->validateEmailAddress($email)) { + throw new InvalidEmailException("Invalid mail address"); + } + + if ($this->userManager->userExists($username)) { + throw new UserAlreadyExistsException('A user with that name already exists.'); + } + + try { + $oldPassword = $password; + if (($password === '') && ($email !== '')) { + /** + * Generate a random password as we are going to have this + * use one time. The new user has to reset it using the link + * from email. + */ + $password = $this->passwordGeneratorService->createPassword(); + } + $user = $this->userManager->createUser($username, $password); + } catch (\Exception $exception) { + throw new CannotCreateUserException("Unable to create user due to exception: {$exception->getMessage()}"); + } + + if ($user === false) { + throw new CannotCreateUserException('Unable to create user.'); + } + + /** + * Send new user mail only if a mail is set + */ + if ($email !== '') { + $user->setEMailAddress($email); + /** + * If the password provided to this method was empty then send email + * to the user notifying the user is created. + */ + if ($oldPassword === '') { + try { + $this->userSendMailService->generateTokenAndSendMail($username, $email); + } catch (\Exception $e) { + $this->logger->error("Can't send new user mail to $email: " . $e->getMessage(), ['app' => 'settings']); + } + } + } + + return $user; + } + + /** + * Add user to the groups + * + * The groups argument could not be empty and it is a list of group names. For example: + * $groups = ['group1', 'group2'] + * + * This function returns the list of group(s) which failed to add user to the group(s) + * + * @param IUser $user + * @param array $groups list of group names, example ['group1', 'group2'] + * @param bool $checkInGroup + * @return array Returns an array of groups which failed to add user + */ + public function addUserToGroups(IUser $user, array $groups, $checkInGroup = true) { + $failedToAdd = []; + + if (\is_array($groups) && \count($groups) > 0) { + foreach ($groups as $groupName) { + $groupObject = $this->groupManager->get($groupName); + + if (empty($groupObject)) { + $groupObject = $this->groupManager->createGroup($groupName); + } + $groupObject->addUser($user); + if ($checkInGroup && !$this->groupManager->isInGroup($user->getUID(), $groupName)) { + $failedToAdd[] = $groupName; + } else { + $this->logger->info('Added userid ' . $user->getUID() . ' to group ' . $groupName, ['app' => 'ocs_api']); + } + } + } + return $failedToAdd; + } + + /** + * Check if the user exist + * + * This function is for convinience. It helps to use this service to check if user exist, + * instead of adding dependency userManager. Kindly do not delete this method. + * + * @param string $uid + * @return bool + */ + public function userExists($uid) { + return $this->userManager->userExists($uid); + } +} diff --git a/lib/private/User/Service/PasswordGeneratorService.php b/lib/private/User/Service/PasswordGeneratorService.php new file mode 100644 index 000000000000..a4edf32f263c --- /dev/null +++ b/lib/private/User/Service/PasswordGeneratorService.php @@ -0,0 +1,66 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OC\User\Service; + +use OCP\Security\ISecureRandom; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\GenericEvent; + +class PasswordGeneratorService { + /** @var EventDispatcherInterface */ + private $eventDispatcher; + /** @var ISecureRandom */ + private $secureRandom; + + /** + * PasswordGeneratorService constructor. + * + * @param EventDispatcherInterface $eventDispatcher + * @param ISecureRandom $secureRandom + */ + public function __construct(EventDispatcherInterface $eventDispatcher, + ISecureRandom $secureRandom) { + $this->eventDispatcher = $eventDispatcher; + $this->secureRandom = $secureRandom; + } + + /** + * Create password + * + * This method will generate password for the user. This method emits a signal which + * could be listened by another function, which could set a random password. Lets say + * if there is no such listener for this signal, then this function itself would + * generate random password of length 20. The idea here is to generate random password. + * + * @return string + */ + public function createPassword() { + $event = new GenericEvent(); + $this->eventDispatcher->dispatch('OCP\User::createPassword', $event); + if ($event->hasArgument('password')) { + $password = $event->getArgument('password'); + } else { + $password = $this->secureRandom->generate(20); + } + return $password; + } +} diff --git a/lib/private/User/Service/UserSendMailService.php b/lib/private/User/Service/UserSendMailService.php new file mode 100644 index 000000000000..f6ade5a9df58 --- /dev/null +++ b/lib/private/User/Service/UserSendMailService.php @@ -0,0 +1,190 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OC\User\Service; + +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\Mail\IMailer; +use OCP\Security\ISecureRandom; +use OCP\User\Exceptions\EmailSendFailedException; +use OCP\User\Exceptions\InvalidUserTokenException; +use OCP\User\Exceptions\UserTokenExpiredException; +use OCP\User\Exceptions\UserTokenMismatchException; +use OCP\Util; + +class UserSendMailService { + /** @var ISecureRandom */ + private $secureRandom; + /** @var IConfig */ + private $config; + /** @var IMailer */ + private $mailer; + /** @var IURLGenerator */ + private $urlGenerator; + /** @var \OC_Defaults */ + private $defaults; + /** @var ITimeFactory */ + private $timeFactory; + /** @var IL10N */ + private $l10n; + + /** + * UserSendMailService constructor. + * + * @param ISecureRandom $secureRandom + * @param IConfig $config + * @param IMailer $mailer + * @param IURLGenerator $urlGenerator + * @param \OC_Defaults $defaults + * @param ITimeFactory $timeFactory + * @param IL10N $l10n + */ + public function __construct(ISecureRandom $secureRandom, IConfig $config, + IMailer $mailer, IURLGenerator $urlGenerator, + \OC_Defaults $defaults, ITimeFactory $timeFactory, + IL10N $l10n) { + $this->secureRandom = $secureRandom; + $this->config = $config; + $this->mailer = $mailer; + $this->urlGenerator = $urlGenerator; + $this->defaults = $defaults; + $this->timeFactory = $timeFactory; + $this->l10n = $l10n; + } + + /** + * @param string $userId + * @param string $email + * @throws \OCP\PreConditionNotMetException + */ + public function generateTokenAndSendMail($userId, $email) { + $fromMailAddress = Util::getDefaultEmailAddress('no-reply'); + $token = $this->secureRandom->generate(21, + ISecureRandom::CHAR_DIGITS, + ISecureRandom::CHAR_LOWER, ISecureRandom::CHAR_UPPER); + $this->config->setUserValue($userId, 'owncloud', + 'lostpassword', $this->timeFactory->getTime() . ':' . $token); + + // data for the mail template + $mailData = [ + 'username' => $userId, + 'url' => $this->urlGenerator->linkToRouteAbsolute('core.user.setPasswordForm', ['userId' => $userId, 'token' => $token]) + ]; + + $mail = new TemplateResponse('core', 'new_user/email-html', $mailData, 'blank'); + $mailContent = $mail->render(); + + $mail = new TemplateResponse('core', 'new_user/email-plain_text', $mailData, 'blank'); + $plainTextMailContent = $mail->render(); + + $subject = $this->l10n->t('Your %s account was created', [$this->defaults->getName()]); + + $message = $this->mailer->createMessage(); + $message->setTo([$email => $userId]); + $message->setSubject($subject); + $message->setHtmlBody($mailContent); + $message->setPlainBody($plainTextMailContent); + $message->setFrom([$fromMailAddress => $this->defaults->getName()]); + $this->mailer->send($message); + } + + /** + * Validates the token for the user + * + * This method validates the token for the user provided. It throws 3 exceptions + * + * @param string $token + * @param IUser $user + * @throws InvalidUserTokenException when an invalid token is provided + * @throws UserTokenExpiredException when the token is expired + * @throws UserTokenMismatchException when the token mismatch happens + */ + public function checkPasswordSetToken($token, IUser $user) { + $splittedToken = \explode(':', $this->config->getUserValue($user->getUID(), 'owncloud', 'lostpassword', null)); + if (\count($splittedToken) !== 2) { + $this->config->deleteUserValue($user->getUID(), 'owncloud', 'lostpassword'); + throw new InvalidUserTokenException('The token provided is invalid.'); + } + + //The value 43200 = 60*60*12 = 1/2 day + if ($splittedToken[0] < ($this->timeFactory->getTime() - (int)$this->config->getAppValue('core', 'token_expire_time', '43200')) || + $user->getLastLogin() > $splittedToken[0]) { + $this->config->deleteUserValue($user->getUID(), 'owncloud', 'lostpassword'); + throw new UserTokenExpiredException('The token provided had expired.'); + } + + if (!\hash_equals($splittedToken[1], $token)) { + throw new UserTokenMismatchException('The token provided is invalid.'); + } + } + + /** + * Send notification email to user + * + * @param IUser $user + * @return bool returns true when an email is sent else false + * @throws EmailSendFailedException + */ + public function sendNotificationMail(IUser $user) { + $email = $user->getEMailAddress(); + $fromMailAddress = Util::getDefaultEmailAddress('no-reply'); + + if ($email !== '') { + try { + $tmpl = new \OC_Template('core', 'lostpassword/notify'); + $msg = $tmpl->fetchPage(); + $tmplAlt = new \OC_Template('core', 'lostpassword/altnotify'); + $msgAlt = $tmplAlt->fetchPage(); + + $message = $this->mailer->createMessage(); + $message->setTo([$email => $user->getUID()]); + $message->setSubject($this->l10n->t('%s password changed successfully', [$this->defaults->getName()])); + $message->setHtmlBody($msg); + $message->setPlainBody($msgAlt); + $message->setFrom([$fromMailAddress => $this->defaults->getName()]); + $this->mailer->send($message); + return true; + } catch (\Exception $exception) { + throw new EmailSendFailedException("Email could not be sent."); + } + } + return false; + } + + /** + * Validates email address + * + * This method is for convenience, as it reduces dependency of adding mailer + * to the caller. The caller could use this service and validate the email. + * Kindly do not delete this method. + * + * @param string $email + * @return bool, True if email address is valid, false otherwise + */ + public function validateEmailAddress($email) { + return $this->mailer->validateMailAddress($email); + } +} diff --git a/lib/public/User/Exceptions/CannotCreateUserException.php b/lib/public/User/Exceptions/CannotCreateUserException.php new file mode 100644 index 000000000000..2065617b6e4b --- /dev/null +++ b/lib/public/User/Exceptions/CannotCreateUserException.php @@ -0,0 +1,31 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCP\User\Exceptions; + +/** + * Class CannotCreateUserException + * + * @package OCP\User\Exceptions + * @since 10.3.0 + */ +class CannotCreateUserException extends \Exception { +} diff --git a/lib/public/User/Exceptions/EmailSendFailedException.php b/lib/public/User/Exceptions/EmailSendFailedException.php new file mode 100644 index 000000000000..067c106e9eb0 --- /dev/null +++ b/lib/public/User/Exceptions/EmailSendFailedException.php @@ -0,0 +1,31 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCP\User\Exceptions; + +/** + * Class EmailSendFailedException + * + * @package OCP\User\Exceptions + * @since 10.3.0 + */ +class EmailSendFailedException extends \Exception { +} diff --git a/lib/public/User/Exceptions/InvalidEmailException.php b/lib/public/User/Exceptions/InvalidEmailException.php new file mode 100644 index 000000000000..a5422c37c2fa --- /dev/null +++ b/lib/public/User/Exceptions/InvalidEmailException.php @@ -0,0 +1,31 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCP\User\Exceptions; + +/** + * Class InvalidEmailException + * + * @package OCP\User\Exceptions + * @since 10.3.0 + */ +class InvalidEmailException extends \Exception { +} diff --git a/lib/public/User/Exceptions/InvalidUserTokenException.php b/lib/public/User/Exceptions/InvalidUserTokenException.php new file mode 100644 index 000000000000..fb1f3438546a --- /dev/null +++ b/lib/public/User/Exceptions/InvalidUserTokenException.php @@ -0,0 +1,42 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCP\User\Exceptions; + +/** + * Class InvalidUserTokenException + * + * @package OCP\User\Exceptions + * @since 10.3.0 + */ +class InvalidUserTokenException extends UserTokenException { + /** + * InvalidUserTokenException constructor. + * + * @param string $message + * @param int $code + * @param \Exception|null $previous + * @since 10.3.0 + */ + public function __construct($message = "", $code = 0, \Exception $previous = null) { + parent::__construct($message, $code, $previous); + } +} diff --git a/lib/public/User/Exceptions/UserAlreadyExistsException.php b/lib/public/User/Exceptions/UserAlreadyExistsException.php new file mode 100644 index 000000000000..399aa69a38c5 --- /dev/null +++ b/lib/public/User/Exceptions/UserAlreadyExistsException.php @@ -0,0 +1,31 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCP\User\Exceptions; + +/** + * Class UserAlreadyExistsException + * + * @package OCP\User\Exceptions + * @since 10.3.0 + */ +class UserAlreadyExistsException extends \Exception { +} diff --git a/lib/public/User/Exceptions/UserTokenException.php b/lib/public/User/Exceptions/UserTokenException.php new file mode 100644 index 000000000000..63d9d77af689 --- /dev/null +++ b/lib/public/User/Exceptions/UserTokenException.php @@ -0,0 +1,43 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCP\User\Exceptions; + +/** + * Class UserTokenException + * + * @package OCP\User\Exceptions + * @since 10.3.0 + */ +class UserTokenException extends \Exception { + /** + * UserTokenException constructor. + * + * @param string $message + * @param int $code + * @param \Exception|null $previous + * @since 10.3.0 + */ + public function __construct($message = "", $code = 0, \Exception $previous = null) { + parent::__construct($message, $code, $previous); + } +} diff --git a/lib/public/User/Exceptions/UserTokenExpiredException.php b/lib/public/User/Exceptions/UserTokenExpiredException.php new file mode 100644 index 000000000000..ab3f3f3f639f --- /dev/null +++ b/lib/public/User/Exceptions/UserTokenExpiredException.php @@ -0,0 +1,42 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCP\User\Exceptions; + +/** + * Class UserTokenExpiredException + * + * @package OCP\User\Exceptions + * @since 10.3.0 + */ +class UserTokenExpiredException extends UserTokenException { + /** + * UserTokenExpiredException constructor. + * + * @param string $message + * @param int $code + * @param \Exception|null $previous + * @since 10.3.0 + */ + public function __construct($message = "", $code = 0, \Exception $previous = null) { + parent::__construct($message, $code, $previous); + } +} diff --git a/lib/public/User/Exceptions/UserTokenMismatchException.php b/lib/public/User/Exceptions/UserTokenMismatchException.php new file mode 100644 index 000000000000..2257df122b3c --- /dev/null +++ b/lib/public/User/Exceptions/UserTokenMismatchException.php @@ -0,0 +1,42 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCP\User\Exceptions; + +/** + * Class UserTokenMismatchException + * + * @package OCP\User\Exceptions + * @since 10.3.0 + */ +class UserTokenMismatchException extends UserTokenException { + /** + * UserTokenMismatchException constructor. + * + * @param string $message + * @param int $code + * @param \Exception|null $previous + * @since 10.3.0 + */ + public function __construct($message = "", $code = 0, \Exception $previous = null) { + parent::__construct($message, $code, $previous); + } +} diff --git a/tests/Core/Command/User/AddTest.php b/tests/Core/Command/User/AddTest.php index 5f09ee5339b5..58cc5f0c5795 100644 --- a/tests/Core/Command/User/AddTest.php +++ b/tests/Core/Command/User/AddTest.php @@ -22,6 +22,9 @@ namespace Tests\Core\Command\User; use OC\Core\Command\User\Add; +use OC\User\Service\CreateUserService; +use OC\User\Service\PasswordGeneratorService; +use OC\User\Service\UserSendMailService; use OC\User\User; use Symfony\Component\Console\Application; use Symfony\Component\Console\Tester\CommandTester; @@ -43,7 +46,22 @@ protected function setUp() { parent::setUp(); $application = new Application(\OC::$server->getConfig(), \OC::$server->getEventDispatcher(), \OC::$server->getRequest()); - $command = new Add(\OC::$server->getUserManager(), \OC::$server->getGroupManager(), \OC::$server->getMailer()); + $command = new Add(\OC::$server->getGroupManager(), + new CreateUserService( + \OC::$server->getUserSession(), \OC::$server->getGroupManager(), + \OC::$server->getUserManager(), \OC::$server->getSecureRandom(), + \OC::$server->getLogger(), + new UserSendMailService( + \OC::$server->getSecureRandom(), \OC::$server->getConfig(), + \OC::$server->getMailer(), \OC::$server->getURLGenerator(), + new \OC_Defaults(), \OC::$server->getTimeFactory(), + \OC::$server->getL10N('settings') + ), + new PasswordGeneratorService( + \OC::$server->getEventDispatcher(), \OC::$server->getSecureRandom() + ) + ) + ); $command->setApplication($application); $this->commandTester = new CommandTester($command); $this->createUser('user1'); @@ -78,7 +96,6 @@ public function inputProvider() { [['uid' => 'user2', '--email' => 'invalidemail'], [], 'Invalid email address supplied'], [['uid' => 'user2', '--password-from-env' => null], [], '--password-from-env given, but OC_PASS is empty!'], /*[['uid' => 'user2'], ['p@ssw0rd', 'password'], 'Passwords did not match'], - [['uid' => 'user2'], ['p@ssw0rd', 'p@ssw0rd'], 'was created successfully'], [['uid' => 'user2', '--display-name' => 'John Doe'], ['p@ssw0rd', 'p@ssw0rd'], 'Display name set to '], [['uid' => 'user2', '--email' => 'user1@example.com'], ['p@ssw0rd', 'p@ssw0rd'], 'Email address set to '], [['uid' => 'user2', '--group' => ['admin']], ['p@ssw0rd', 'p@ssw0rd'], 'added to group '],*/ diff --git a/tests/Core/Controller/UserControllerTest.php b/tests/Core/Controller/UserControllerTest.php new file mode 100644 index 000000000000..3063a38ce6fb --- /dev/null +++ b/tests/Core/Controller/UserControllerTest.php @@ -0,0 +1,477 @@ + + * + * @copyright Copyright (c) 2018, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace Tests\Core\Controller; + +use OC\AppFramework\Http; +use OC\Core\Controller\UserController; +use OC\User\Service\UserSendMailService; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IL10N; +use OCP\ILogger; +use OCP\IRequest; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserManager; +use OCP\User\Exceptions\EmailSendFailedException; +use OCP\User\Exceptions\InvalidUserTokenException; +use OCP\User\Exceptions\UserTokenException; +use OCP\User\Exceptions\UserTokenExpiredException; +use OCP\User\Exceptions\UserTokenMismatchException; +use Test\TestCase; + +/** + * Class UserControllerTest + * + * @group DB + * @package Tests\Core\Controller + */ +class UserControllerTest extends TestCase { + /** @var IRequest | \PHPUnit_Framework_MockObject_MockObject */ + private $request; + /** @var IUserManager | \PHPUnit_Framework_MockObject_MockObject */ + private $userManager; + /** @var \OC_Defaults | \PHPUnit_Framework_MockObject_MockObject */ + private $defaults; + /** @var UserSendMailService | \PHPUnit_Framework_MockObject_MockObject */ + private $userSendMailService; + /** @var IURLGenerator | \PHPUnit_Framework_MockObject_MockObject */ + private $urlGenerator; + /** @var ILogger | \PHPUnit_Framework_MockObject_MockObject */ + private $logger; + /** @var IL10N | \PHPUnit_Framework_MockObject_MockObject */ + private $l10n; + /** @var UserController */ + private $userController; + + public function setUp() { + parent::setUp(); + + $this->request = $this->createMock(IRequest::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->defaults = $this->createMock(\OC_Defaults::class); + $this->userSendMailService = $this->createMock(UserSendMailService::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->logger = $this->createMock(ILogger::class); + $this->l10n = $this->createMock(IL10N::class); + $this->userController = new UserController('core', $this->request, + $this->userManager, $this->defaults, $this->userSendMailService, + $this->urlGenerator, $this->logger, $this->l10n); + } + + public function testSetPasswordForm() { + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('foo'); + + $this->userManager->method('get') + ->with('foo') + ->willReturn($user); + + $this->urlGenerator->method('linkToRouteAbsolute') + ->willReturn('http://localhost/setpassword/1234/foo'); + + $expectedResult = new TemplateResponse( + 'core', 'new_user/setpassword', + ['link' => 'http://localhost/setpassword/1234/foo'], + 'guest' + ); + $result = $this->userController->setPasswordForm('abc111foo', 'foo'); + $this->assertEquals($expectedResult, $result); + } + + public function providesExpception() { + return [ + ['UserTokenExpiredException'], + ['InvalidUserTokenException'], + ['UserTokenMismatchException'] + ]; + } + + /** + * @dataProvider providesExpception + */ + public function testSetPasswordFormUserTokenExpiredException($exception) { + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('foo'); + + $this->userManager->method('get') + ->with('foo') + ->willReturn($user); + + if ($exception === 'UserTokenExpiredException') { + $this->userSendMailService->method('checkPasswordSetToken') + ->willThrowException(new UserTokenExpiredException()); + $this->urlGenerator->method('linkToRouteAbsolute') + ->willReturn('http://localhost/resendToken/1234/foo'); + + $expectedResult = new TemplateResponse( + 'core', 'new_user/resendtokenbymail', + ['link' => 'http://localhost/resendToken/1234/foo'], + 'guest' + ); + } elseif (($exception === 'InvalidUserTokenException') || ($exception === 'UserTokenMismatchException')) { + $this->userSendMailService->method('checkPasswordSetToken') + ->willThrowException(new InvalidUserTokenException('The token provided is invalid.')); + $this->l10n->method('t') + ->willReturn('The token provided is invalid.'); + $expectedResult = new TemplateResponse( + 'core', 'error', + ["errors" => [["error" => 'The token provided is invalid.']]], + 'guest' + ); + } + + $result = $this->userController->setPasswordForm('abc111foo', 'foo'); + $this->assertEquals($expectedResult, $result); + } + + public function testResendToken() { + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('foo'); + $user->method('getEMailAddress') + ->willReturn('foo@bar.com'); + + $this->userManager->method('get') + ->with('foo') + ->willReturn($user); + + $expectedResult = new TemplateResponse( + 'core', 'new_user/tokensendnotify', [], 'guest' + ); + $result = $this->userController->resendToken('foo'); + $this->assertEquals($expectedResult, $result); + } + + public function providesFailureForResendToken() { + return [ + ['user'], + ['email'], + ['sendmail'] + ]; + } + + /** + * @dataProvider providesFailureForResendToken + */ + public function testResendTokenFail($data) { + $user = $this->createMock(IUser::class); + if ($data === 'user') { + $this->userManager->method('get') + ->with('foo') + ->willReturn(null); + } elseif ($data === 'email') { + $this->userManager->method('get') + ->with('foo') + ->willReturn($user); + } elseif ($data === 'sendmail') { + $user->method('getEMailAddress') + ->willReturn('foo@bar.com'); + $this->userManager->method('get') + ->with('foo') + ->willReturn($user); + $this->userSendMailService->method('generateTokenAndSendMail') + ->willThrowException(new \Exception()); + } + + if (($data === 'user') || ($data === 'email')) { + $this->l10n->method('t') + ->willReturn('Failed to create activation link. Please contact your administrator.'); + $expectedResult = new TemplateResponse( + 'core', 'error', + [ + "errors" => [["error" => 'Failed to create activation link. Please contact your administrator.']] + ], + 'guest' + ); + } else { + $this->l10n->method('t') + ->willReturn('Can\'t send email to the user. Contact your administrator.'); + $expectedResult = new TemplateResponse( + 'core', 'error', + [ + "errors" => [["error" => 'Can\'t send email to the user. Contact your administrator.']] + ], 'guest' + ); + } + + $result = $this->userController->resendToken('foo'); + $this->assertEquals($expectedResult, $result); + } + + public function testSetPassword() { + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('foo'); + $user->method('setPassword') + ->willReturn(true); + + $this->userManager->method('get') + ->with('foo') + ->willReturn($user); + + $expectedResult = new JSONResponse(['status' => 'success']); + $result = $this->userController->setPassword('abc123foo', 'foo', 'foopass'); + $this->assertEquals($expectedResult, $result); + } + + public function testSetPasswordNullUserExcception() { + $this->userManager->method('get') + ->willReturn(null); + $this->l10n->method('t') + ->willReturn('Failed to set password. Please contact the administrator.'); + $result = $this->userController->setPassword('fooBaZ1', 'foo', '123'); + $this->assertEquals( + new JSONResponse( + [ + 'status' => 'error', + 'message' => 'Failed to set password. Please contact the administrator.', + 'type' => 'usererror' + ], Http::STATUS_NOT_FOUND + ), $result); + } + + public function testSetPasswordInvalidTokenExcception() { + $user = $this->createMock(IUser::class); + $this->userManager->method('get') + ->with('foo') + ->willReturn($user); + + $this->userSendMailService->method('checkPasswordSetToken') + ->will($this->throwException(new UserTokenException('The token provided is invalid.'))); + + $result = $this->userController->setPassword('fooBaZ1', 'foo', '123'); + $this->assertEquals(new JSONResponse( + [ + 'status' => 'error', + 'message' => 'The token provided is invalid.', + 'type' => 'tokenfailure' + ], Http::STATUS_UNAUTHORIZED + ), $result); + } + + public function testSetPasswordPolicyException() { + $user = $this->createMock(IUser::class); + $user->method('setPassword') + ->willReturn(false); + $this->userManager->method('get') + ->with('foo') + ->willReturn($user); + + $this->logger + ->expects($this->once()) + ->method('error') + ->with('The password can not be set for user: foo'); + + $this->l10n->method('t') + ->willReturn('Failed to set password. Please contact your administrator.'); + + $expectedResult = new JSONResponse( + [ + 'status' => 'error', + 'message' => 'Failed to set password. Please contact your administrator.', + 'type' => 'passwordsetfailed', + ], Http::STATUS_FORBIDDEN + ); + $result = $this->userController->setPassword('fooBaZ1', 'foo', '123'); + $this->assertEquals($expectedResult, $result); + } + + public function testSetPasswordFailureOnNullUser() { + $this->l10n->method('t') + ->willReturn('Failed to set password. Please contact the administrator.'); + $expectedResult = new JSONResponse( + [ + 'status' => 'error', + 'message' => 'Failed to set password. Please contact the administrator.', + 'type' => 'usererror' + ], Http::STATUS_NOT_FOUND + ); + $result = $this->userController->setPassword('abc123foo', 'foo', 'foopass'); + $this->assertEquals($expectedResult, $result); + } + + public function providesSetPasswordException() { + return [ + ['tokenException'], + ['sendMail'] + ]; + } + + /** + * @dataProvider providesSetPasswordException + */ + public function testSetPasswordForException($causeOfException) { + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('foo'); + $user->method('setPassword') + ->willReturn(true); + + $this->userManager->method('get') + ->willReturn($user); + + if ($causeOfException === 'tokenException') { + $this->userSendMailService->method('checkPasswordSetToken') + ->willThrowException(new UserTokenException('', 0)); + $expectedResult = new JSONResponse( + [ + 'status' => 'error', + 'message' => '', + 'type' => 'tokenfailure' + ], Http::STATUS_UNAUTHORIZED + ); + } elseif ($causeOfException === 'sendMail') { + $this->userSendMailService->method('sendNotificationMail') + ->willThrowException(new EmailSendFailedException()); + $this->l10n->method('t') + ->willReturn('Failed to send email. Please contact your administrator.'); + $expectedResult = new JSONResponse( + [ + 'status' => 'error', + 'message' => 'Failed to send email. Please contact your administrator.', + 'type' => 'emailsendfailed' + ], Http::STATUS_INTERNAL_SERVER_ERROR + ); + } + + $result = $this->userController->setPassword('abc123Foo', 'foo', 'fooPass'); + $this->assertEquals($expectedResult, $result); + } + + public function providesUserTokenExceptionData() { + return [ + ['invalid_token'], + ['expired_token'], + ['mismatch_token'] + ]; + } + + /** + * @dataProvider providesUserTokenExceptionData + */ + public function testSetPasswordFormExceptionResponse($tokenException) { + $user = $this->createMock(IUser::class); + + $this->userManager->expects($this->once()) + ->method('get') + ->with('foo') + ->willReturn($user); + + $this->l10n->method('t') + ->will($this->onConsecutiveCalls( + "The token provided is invalid.", + "The token provided is invalid." + )); + + if ($tokenException === 'expired_token') { + $this->urlGenerator->expects($this->once()) + ->method('linkToRouteAbsolute') + ->willReturn('http://localhost/core/setpassword/form/1234/foo'); + $this->userSendMailService->method('checkPasswordSetToken') + ->will($this->throwException(new UserTokenExpiredException('The token provided had expired.'))); + + $result = $this->userController->setPasswordForm('fooBaZ1', 'foo'); + $this->assertEquals( + new TemplateResponse('core', 'new_user/resendtokenbymail', + ['link' => 'http://localhost/core/setpassword/form/1234/foo'], + 'guest'), $result); + } elseif ($tokenException === 'mismatch_token') { + $this->userSendMailService->method('checkPasswordSetToken') + ->will($this->throwException(new UserTokenMismatchException('The token provided is invalid.'))); + $result = $this->userController->setPasswordForm('fooBaZ1', 'foo'); + $this->assertEquals( + new TemplateResponse( + 'core', 'error', + ['errors' => [['error' => 'The token provided is invalid.']]], 'guest'), $result + ); + } elseif ($tokenException === 'invalid_token') { + $this->userSendMailService->method('checkPasswordSetToken') + ->will($this->throwException(new InvalidUserTokenException('The token provided is invalid.'))); + $result = $this->userController->setPasswordForm('fooBaZ1', 'foo'); + $this->assertEquals( + new TemplateResponse('core', 'error', + [ + "errors" => [["error" => "The token provided is invalid."]] + ], 'guest'), $result); + } + } + + public function testResendTokenNullUserResponse() { + $this->userManager->method('get') + ->willReturn(null); + $this->l10n->method('t') + ->willReturn('Failed to create activation link. Please contact your administrator.'); + $result = $this->userController->resendToken('foo'); + $this->assertEquals( + new TemplateResponse( + 'core', 'error', + ["errors" => [["error" =>"Failed to create activation link. Please contact your administrator."]]], + 'guest'), $result); + } + + public function testResendTokenEmailNotSendResponse() { + $user = $this->createMock(IUser::class); + $user->method('getEMailAddress') + ->willReturn(null); + + $this->userManager->expects($this->once()) + ->method('get') + ->willReturn($user); + + $this->l10n->method('t') + ->willReturn('Failed to create activation link. Please contact your administrator.'); + + $result = $this->userController->resendToken('foo'); + $this->assertEquals( + new TemplateResponse( + 'core', 'error', + ["errors" => [["error" =>"Failed to create activation link. Please contact your administrator."]]], + 'guest'), $result); + } + + public function testResendTokenSendMailFailedResponse() { + $user = $this->createMock(IUser::class); + + $user->method('getEMailAddress') + ->willReturn('foo@bar.com'); + + $this->userManager->expects($this->once()) + ->method('get') + ->willReturn($user); + + $this->userSendMailService->method('generateTokenAndSendMail') + ->will($this->throwException(new \Exception("Cannot send email."))); + + $this->l10n->method('t') + ->willReturn('Can\'t send email to the user. Contact your administrator.'); + + $result = $this->userController->resendToken('foo'); + $this->assertEquals( + new TemplateResponse( + 'core', 'error', + ["errors" => [["error" =>"Can't send email to the user. Contact your administrator."]]], + 'guest'), $result); + } +} diff --git a/tests/lib/User/Service/CreateUserServiceTest.php b/tests/lib/User/Service/CreateUserServiceTest.php new file mode 100644 index 000000000000..17998f96c8f4 --- /dev/null +++ b/tests/lib/User/Service/CreateUserServiceTest.php @@ -0,0 +1,173 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace Test\User\Service; + +use OC\User\Service\CreateUserService; +use OC\User\Service\PasswordGeneratorService; +use OC\User\Service\UserSendMailService; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\ILogger; +use OCP\IUser; +use OCP\IUserManager; +use OCP\IUserSession; +use OCP\Mail\IMailer; +use OCP\Security\ISecureRandom; +use Test\TestCase; + +class CreateUserServiceTest extends TestCase { + /** @var IUserSession | \PHPUnit_Framework_MockObject_MockObject */ + private $userSession; + /** @var IGroupManager | \PHPUnit_Framework_MockObject_MockObject */ + private $groupManager; + /** @var IUserManager | \PHPUnit_Framework_MockObject_MockObject */ + private $userManager; + /** @var IMailer | \PHPUnit_Framework_MockObject_MockObject */ + private $mailer; + /** @var ISecureRandom | \PHPUnit_Framework_MockObject_MockObject */ + private $secureRandom; + /** @var ILogger | \PHPUnit_Framework_MockObject_MockObject */ + private $logger; + /** @var UserSendMailService | \PHPUnit_Framework_MockObject_MockObject */ + private $userSendMailService; + /** @var PasswordGeneratorService | \PHPUnit_Framework_MockObject_MockObject */ + private $passwordGerneratorService; + /** @var CreateUserService */ + private $createUserService; + + protected function setUp() { + parent::setUp(); + + $this->userSession = $this->createMock(IUserSession::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->mailer = $this->createMock(IMailer::class); + $this->secureRandom = $this->createMock(ISecureRandom::class); + $this->logger = $this->createMock(ILogger::class); + $this->userSendMailService = $this->createMock(UserSendMailService::class); + $this->passwordGerneratorService = $this->createMock(PasswordGeneratorService::class); + $this->createUserService = new CreateUserService($this->userSession, $this->groupManager, + $this->userManager, $this->secureRandom, $this->logger, + $this->userSendMailService, $this->passwordGerneratorService); + } + + public function testCreateUserWithEmail() { + $this->userSendMailService->method('validateEmailAddress') + ->willReturn(true); + $currentUser = $this->createMock(IUser::class); + $currentUser->method('getUID') + ->willReturn('user1'); + $newUser = $this->createMock(IUser::class); + $newUser->method('getUID') + ->willReturn('foo'); + + $this->userSession->method('getUser') + ->willReturn($currentUser); + + $this->userManager->method('userExists') + ->with('foo') + ->willReturn(false); + + $this->secureRandom->method('generate') + ->willReturn('aBcDeFgH'); + + $this->userManager->method('createUser') + ->willReturn($newUser); + + $result = $this->createUserService->createUser(['username' => 'foo', 'password' => '', 'email' => 'foo@bar.com']); + $this->assertInstanceOf(IUser::class, $result); + $this->assertEquals('foo', $result->getUID()); + } + + public function testAddUserToGroups() { + $newuser = $this->createMock(IUser::class); + $newuser->method('getUID') + ->willReturn('foo'); + + $this->groupManager->method('isAdmin') + ->willReturn(true); + $group = $this->createMock(IGroup::class); + $this->groupManager->method('get') + ->willReturn($group); + $this->groupManager->method('isInGroup') + ->willReturn(true); + + $failedToAddGroups = $this->createUserService->addUserToGroups($newuser, ['group1']); + $this->assertCount(0, $failedToAddGroups); + } + + /** + * @expectedExceptionMessage Invalid mail address + * @expectedException \OCP\User\Exceptions\InvalidEmailException + */ + public function testInvalidEmail() { + $this->mailer->method('validateMailAddress') + ->willReturn(false); + $this->createUserService->createUser(['username' => 'foo', 'password' => '', 'email' => 'foo@bar']); + } + + /** + * @expectedExceptionMessage A user with that name already exists. + * @expectedException \OCP\User\Exceptions\UserAlreadyExistsException + */ + public function testAlreadyExistingUser() { + $this->userSendMailService->method('validateEmailAddress') + ->willReturn(true); + + $this->userManager->method('userExists') + ->willReturn(true); + + $this->createUserService->createUser(['username' => 'foo', 'password' => '', 'email' =>'foo@bar.com']); + } + + /** + * @expectedExceptionMessage Unable to create user due to exception: + * @expectedException \OCP\User\Exceptions\CannotCreateUserException + */ + public function testUserCreateException() { + $this->userSendMailService->method('validateEmailAddress') + ->willReturn(true); + + $this->userManager->method('userExists') + ->willReturn(false); + + $this->userManager->method('createUser') + ->willThrowException(new \Exception()); + $this->createUserService->createUser(['username' => 'foo', 'password' => '', 'email' => 'foo@bar.com']); + } + + /** + * @expectedExceptionMessage Unable to create user. + * @expectedException \OCP\User\Exceptions\CannotCreateUserException + */ + public function testUserCreateFailed() { + $this->userSendMailService->method('validateEmailAddress') + ->willReturn(true); + + $this->userManager->method('userExists') + ->willReturn(false); + + $this->userManager->method('createUser') + ->willReturn(false); + $this->createUserService->createUser(['username' => 'foo', 'password' => '', 'email' => 'foo@bar.com']); + } +} diff --git a/tests/lib/User/Service/PasswordGeneratorServiceTest.php b/tests/lib/User/Service/PasswordGeneratorServiceTest.php new file mode 100644 index 000000000000..470153934358 --- /dev/null +++ b/tests/lib/User/Service/PasswordGeneratorServiceTest.php @@ -0,0 +1,62 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace Test\User\Service; + +use OC\User\Service\PasswordGeneratorService; +use OCP\Security\ISecureRandom; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\GenericEvent; +use Test\TestCase; + +class PasswordGeneratorServiceTest extends TestCase { + /** @var EventDispatcherInterface */ + private $eventDispatcher; + + /** @var ISecureRandom | \PHPUnit_Framework_MockObject_MockObject */ + private $secureRandom; + + /** @var PasswordGeneratorService */ + private $passwordGeneratorService; + + protected function setUp() { + parent::setUp(); + $this->eventDispatcher = new EventDispatcher(); + $this->secureRandom = $this->createMock(ISecureRandom::class); + $this->passwordGeneratorService = new PasswordGeneratorService($this->eventDispatcher, $this->secureRandom); + } + + public function testCreatePasswordWithoutListener() { + $this->secureRandom->method('generate') + ->willReturn('123456'); + $this->assertEquals('123456', $this->passwordGeneratorService->createPassword()); + } + + public function testCreatePasswordWithListener() { + $passwordTobeSet = "foobar123"; + $this->eventDispatcher->addListener('OCP\User::createPassword', function (GenericEvent $event) use ($passwordTobeSet) { + $event->setArgument('password', $passwordTobeSet); + }); + + $this->assertEquals($passwordTobeSet, $this->passwordGeneratorService->createPassword()); + } +} diff --git a/tests/lib/User/Service/UserSendMailServiceTest.php b/tests/lib/User/Service/UserSendMailServiceTest.php new file mode 100644 index 000000000000..a791524fac13 --- /dev/null +++ b/tests/lib/User/Service/UserSendMailServiceTest.php @@ -0,0 +1,241 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace Test\User\Service; + +use OC\Mail\Message; +use OC\User\Service\UserSendMailService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\Mail\IMailer; +use OCP\Security\ISecureRandom; +use Test\TestCase; + +class UserSendMailServiceTest extends TestCase { + /** @var ISecureRandom */ + private $secureRandom; + + /** @var IConfig | \PHPUnit_Framework_MockObject_MockObject */ + private $config; + + /** @var IMailer | \PHPUnit_Framework_MockObject_MockObject */ + private $mailer; + + /** @var IURLGenerator | \PHPUnit_Framework_MockObject_MockObject */ + private $urlGenerator; + + /** @var \OC_Defaults | \PHPUnit_Framework_MockObject_MockObject */ + private $defaults; + + /** @var ITimeFactory | \PHPUnit_Framework_MockObject_MockObject */ + private $timeFactory; + + /** @var IL10N | \PHPUnit_Framework_MockObject_MockObject */ + private $l10n; + + /** @var UserSendMailService */ + private $userSendMailService; + + protected function setUp() { + parent::setUp(); + + $this->secureRandom = $this->createMock(ISecureRandom::class); + $this->config = $this->createMock(IConfig::class); + $this->mailer = $this->createMock(IMailer::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->defaults = $this->createMock(\OC_Defaults::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->l10n = $this->createMock(IL10N::class); + $this->userSendMailService = new UserSendMailService($this->secureRandom, + $this->config, $this->mailer, $this->urlGenerator, $this->defaults, + $this->timeFactory, $this->l10n); + } + + public function testGenerateTokenAndSendMail() { + $this->secureRandom->method('generate') + ->willReturn('123456'); + $this->urlGenerator->method('linkToRouteAbsolute') + ->willReturn('http://localhost/setPasswordForm/1234/foo'); + + $message = $this->createMock(Message::class); + $message->expects($this->once()) + ->method('setTo'); + $message->expects($this->once()) + ->method('setSubject'); + $message->expects($this->once()) + ->method('setHtmlBody'); + $message->expects($this->once()) + ->method('setPlainBody'); + $message->expects($this->once()) + ->method('setFrom'); + + $this->mailer->method('createMessage') + ->willReturn($message); + $this->mailer->expects($this->once()) + ->method('send'); + + $this->userSendMailService->generateTokenAndSendMail('foo', 'foo@barr.com'); + } + + /** + * @expectedException OCP\User\Exceptions\InvalidUserTokenException + * @expectedExceptionMessage The token provided is invalid. + */ + public function testcheckPasswordSetInvalidToken() { + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('foo'); + + $this->config->method('getUserValue') + ->willReturn('fooBaz1'); + $this->userSendMailService->checkPasswordSetToken('fooBaz1', $user); + } + + /** + * @expectedException \OCP\User\Exceptions\UserTokenExpiredException + * @expectedExceptionMessage The token provided had expired. + */ + public function testCheckPasswordSetTokenExpired() { + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('foo'); + + $this->config->method('getUserValue') + ->willReturn('1234:foobaz'); + + $this->timeFactory->method('getTime') + ->willReturn('1234567'); + $this->config->method('getAppValue') + ->willReturn('123'); + + $this->userSendMailService->checkPasswordSetToken('1234:foobaz', $user); + } + + /** + * @expectedException \OCP\User\Exceptions\UserTokenMismatchException + * @expectedExceptionMessage The token provided is invalid. + */ + public function testCheckPasswordSetTokenMismatch() { + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('foo'); + + $this->config->method('getUserValue') + ->willReturn('12345678:foobaz'); + + $this->timeFactory->method('getTime') + ->willReturn('12345'); + $this->config->method('getAppValue') + ->willReturn('1234'); + + $this->userSendMailService->checkPasswordSetToken('1234:foobaz', $user); + } + + public function testSendNotificationMailSuccess() { + $user = $this->createMock(IUser::class); + $user->method('getEMailAddress') + ->willReturn('foo@barr.com'); + + $message = $this->createMock(Message::class); + $message->expects($this->once()) + ->method('setTo'); + $message->expects($this->once()) + ->method('setSubject'); + $message->expects($this->once()) + ->method('setHtmlBody'); + $message->expects($this->once()) + ->method('setPlainBody'); + $message->expects($this->once()) + ->method('setFrom'); + + $this->mailer->method('createMessage') + ->willReturn($message); + $this->mailer->expects($this->once()) + ->method('send') + ->with($message); + + $this->assertTrue($this->userSendMailService->sendNotificationMail($user)); + } + + public function testSendNotificationMailFail() { + $user = $this->createMock(IUser::class); + $user->method('getEMailAddress') + ->willReturn(''); + + $this->assertFalse($this->userSendMailService->sendNotificationMail($user)); + } + + /** + * @expectedException \OCP\User\Exceptions\EmailSendFailedException + * @expectedExceptionMessage Email could not be sent. + */ + public function testSendNotificationMailSendFail() { + $user = $this->createMock(IUser::class); + $user->method('getEMailAddress') + ->willReturn('foo@barr.com'); + + $message = $this->createMock(Message::class); + $message->expects($this->once()) + ->method('setTo'); + $message->expects($this->once()) + ->method('setSubject'); + $message->expects($this->once()) + ->method('setHtmlBody'); + $message->expects($this->once()) + ->method('setPlainBody'); + $message->expects($this->once()) + ->method('setFrom'); + + $this->mailer->method('createMessage') + ->willReturn($message); + $this->mailer->expects($this->once()) + ->method('send') + ->will($this->throwException(new \Exception("Failed"))); + + $this->assertTrue($this->userSendMailService->sendNotificationMail($user)); + } + + public function providesValidateEmailAddress() { + return [ + ['foo@bar.com', true], + ['foo@bar', false], + ]; + } + + /** + * This is a test for a convenience method + * + * @param string $emailAddress + * @param bool $expectedResult + * + * @dataProvider providesValidateEmailAddress + */ + public function testValidateEmailAddress($emailAddress, $expectedResult) { + $this->mailer->method('validateMailAddress') + ->with($emailAddress) + ->willReturn($expectedResult); + + $this->assertEquals($expectedResult, $this->userSendMailService->validateEmailAddress($emailAddress)); + } +}