From 4e1577d1dbf2d5c5879bf11a7ea4fe2715e3f220 Mon Sep 17 00:00:00 2001 From: Sujith H Date: Wed, 15 Aug 2018 20:33:37 +0530 Subject: [PATCH] [stable10] Backport of resetpassword command supports send password reset link and display link The resetpassword supports options to 1) display the password reset link in command line. 2) send email to reset the password for user and display the link in the command line. Signed-off-by: Sujith H --- core/Command/User/ResetPassword.php | 65 +++++- core/Controller/LostController.php | 33 ++- core/register_command.php | 7 +- tests/Core/Command/User/ResetPasswordTest.php | 214 ++++++++++++++++++ 4 files changed, 305 insertions(+), 14 deletions(-) create mode 100644 tests/Core/Command/User/ResetPasswordTest.php diff --git a/core/Command/User/ResetPassword.php b/core/Command/User/ResetPassword.php index f78441555c58..eb436955d6cf 100644 --- a/core/Command/User/ResetPassword.php +++ b/core/Command/User/ResetPassword.php @@ -7,6 +7,7 @@ * @author Laurens Post * @author Morris Jobke * @author Thomas Müller + * @author Sujith Haridasan * * @copyright Copyright (c) 2018, ownCloud GmbH * @license AGPL-3.0 @@ -27,7 +28,16 @@ namespace OC\Core\Command\User; +use OC\Core\Controller\LostController; +use OC\Helper\EnvironmentHelper; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IURLGenerator; use OCP\IUserManager; +use OCP\Mail\IMailer; +use OCP\Security\ISecureRandom; +use OCP\Util; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; @@ -39,9 +49,25 @@ class ResetPassword extends Command { /** @var IUserManager */ protected $userManager; + /** @var IConfig */ + private $config; + /** @var ITimeFactory */ + private $timeFactory; + /** @var EnvironmentHelper */ + private $environmentHelper; + /** @var LostController */ + private $lostController; - public function __construct(IUserManager $userManager) { + public function __construct(IUserManager $userManager, + IConfig $config, + ITimeFactory $timeFactory, + EnvironmentHelper $environmentHelper, + LostController $lostController) { $this->userManager = $userManager; + $this->config = $config; + $this->timeFactory = $timeFactory; + $this->environmentHelper = $environmentHelper; + $this->lostController = $lostController; parent::__construct(); } @@ -60,11 +86,25 @@ protected function configure() { InputOption::VALUE_NONE, 'Read the password from the OC_PASS environment variable.' ) + ->addOption( + 'send-email', + null, + InputOption::VALUE_NONE, + 'The email-id set while creating the user, will be used to send link for password reset. This option will also display the link sent to user.' + ) + ->addOption( + 'output-link', + null, + InputOption::VALUE_NONE, + 'The link to reset the password will be displayed.' + ) ; } protected function execute(InputInterface $input, OutputInterface $output) { $username = $input->getArgument('user'); + $emailLink = $input->getOption('send-email'); + $displayLink = $input->getOption('output-link'); /** @var $user \OCP\IUser */ $user = $this->userManager->get($username); @@ -74,11 +114,32 @@ protected function execute(InputInterface $input, OutputInterface $output) { } if ($input->getOption('password-from-env')) { - $password = \getenv('OC_PASS'); + $password = $this->environmentHelper->getEnvVar('OC_PASS'); if (!$password) { $output->writeln('--password-from-env given, but OC_PASS is empty!'); return 1; } + } elseif ($emailLink || $displayLink) { + $userId = $user->getUID(); + list($link, $token) = $this->lostController->generateTokenAndLink($userId); + + if ($emailLink && $user->getEMailAddress() !== null) { + try { + $this->config->deleteUserValue($userId, 'owncloud', 'lostpassword'); + $this->lostController->sendEmail($userId, $token, $link); + } catch (\Exception $e) { + $output->writeln('Can\'t send new user mail to ' . $user->getEMailAddress() . ': ' . $e->getMessage() . ''); + return 1; + } + } else { + if ($emailLink) { + $output->writeln('Email address is not set for the user ' . $user->getUID() . ''); + return 1; + } + } + $this->config->setUserValue($userId, 'owncloud', 'lostpassword', $this->timeFactory->getTime() . ':' . $token); + $output->writeln('The password reset link is: ' . $link); + return 0; } elseif ($input->isInteractive()) { /** @var $dialog \Symfony\Component\Console\Helper\QuestionHelper */ $dialog = $this->getHelperSet()->get('question'); diff --git a/core/Controller/LostController.php b/core/Controller/LostController.php index 0d393f906920..ad8fe8d16962 100644 --- a/core/Controller/LostController.php +++ b/core/Controller/LostController.php @@ -205,7 +205,8 @@ private function success() { public function email($user) { // FIXME: use HTTP error codes try { - $this->sendEmail($user); + list($link, $token) = $this->generateTokenAndLink($user); + $this->sendEmail($user, $token, $link); } catch (\Exception $e) { return $this->error($e->getMessage()); } @@ -278,12 +279,28 @@ protected function sendNotificationMail($userId) { } } + /** + * @param string $userId + * @return array + */ + public function generateTokenAndLink($userId) { + $token = $this->secureRandom->generate(21, + ISecureRandom::CHAR_DIGITS . + ISecureRandom::CHAR_LOWER . + ISecureRandom::CHAR_UPPER); + + $link = $this->urlGenerator->linkToRouteAbsolute('core.lost.resetform', ['userId' => $userId, 'token' => $token]); + return [$link, $token]; + } + /** * @param string $user + * @param string $token + * @param string $link * @throws \Exception * @return boolean */ - protected function sendEmail($user) { + public function sendEmail($user, $token, $link) { if ($this->userManager->userExists($user)) { $userObject = $this->userManager->get($user); $email = $userObject->getEMailAddress(); @@ -310,23 +327,17 @@ protected function sendEmail($user) { } } - $token = $this->config->getUserValue($user, 'owncloud', 'lostpassword'); - if ($token !== '') { - $splittedToken = \explode(':', $token); + $getToken = $this->config->getUserValue($user, 'owncloud', 'lostpassword'); + if ($getToken !== '') { + $splittedToken = \explode(':', $getToken); if ((\count($splittedToken)) === 2 && $splittedToken[0] > ($this->timeFactory->getTime() - 60 * 5)) { $this->logger->alert('The email is not sent because a password reset email was sent recently.'); return false; } } - $token = $this->secureRandom->generate(21, - ISecureRandom::CHAR_DIGITS . - ISecureRandom::CHAR_LOWER . - ISecureRandom::CHAR_UPPER); $this->config->setUserValue($user, 'owncloud', 'lostpassword', $this->timeFactory->getTime() . ':' . $token); - $link = $this->urlGenerator->linkToRouteAbsolute('core.lost.resetform', ['userId' => $user, 'token' => $token]); - $tmpl = new \OC_Template('core', 'lostpassword/email'); $tmpl->assign('link', $link); $msg = $tmpl->fetchPage(); diff --git a/core/register_command.php b/core/register_command.php index 0d475f09f8ba..59490fec0c4c 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -142,7 +142,12 @@ $application->add(new OC\Core\Command\User\ListUsers(\OC::$server->getUserManager())); $application->add(new OC\Core\Command\User\ListUserGroups(\OC::$server->getUserManager(), \OC::$server->getGroupManager())); $application->add(new OC\Core\Command\User\Report(\OC::$server->getUserManager())); - $application->add(new OC\Core\Command\User\ResetPassword(\OC::$server->getUserManager())); + $application->add(new OC\Core\Command\User\ResetPassword(\OC::$server->getUserManager(), \OC::$server->getConfig(), \OC::$server->getTimeFactory(), + new \OC\Helper\EnvironmentHelper(), new \OC\Core\Controller\LostController('settings', + \OC::$server->getRequest(), \OC::$server->getURLGenerator(), \OC::$server->getUserManager(), new \OC_Defaults(), + \OC::$server->getL10N('settings'), \OC::$server->getConfig(), \OC::$server->getSecureRandom(), + \OCP\Util::getDefaultEmailAddress('lostpassword-noreply'), \OC::$server->getEncryptionManager()->isEnabled(), + \OC::$server->getMailer(), \OC::$server->getTimeFactory(), \OC::$server->getLogger(), \OC::$server->getUserSession()))); $application->add(new OC\Core\Command\User\Setting(\OC::$server->getUserManager(), \OC::$server->getConfig(), \OC::$server->getDatabaseConnection())); $application->add(new OC\Core\Command\User\Modify(\OC::$server->getUserManager(), \OC::$server->getMailer())); $application->add(new OC\Core\Command\User\SyncBackend(\OC::$server->getAccountMapper(), \OC::$server->getConfig(), \OC::$server->getUserManager(), \OC::$server->getLogger())); diff --git a/tests/Core/Command/User/ResetPasswordTest.php b/tests/Core/Command/User/ResetPasswordTest.php new file mode 100644 index 000000000000..14b92c926d93 --- /dev/null +++ b/tests/Core/Command/User/ResetPasswordTest.php @@ -0,0 +1,214 @@ + + * + * @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\Command\User; + +use OC\Core\Command\User\ResetPassword; +use OC\Core\Controller\LostController; +use OC\Helper\EnvironmentHelper; +use OC\Mail\Message; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Mail\IMailer; +use OCP\Security\ISecureRandom; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Test\TestCase; + +/** + * Class ResetPasswordTest + * + * @group DB + * @package Tests\Core\Command\User + */ +class ResetPasswordTest extends TestCase { + /** @var IUserManager | \PHPUnit_Framework_MockObject_MockObject */ + private $userManager; + /** @var IConfig | \PHPUnit_Framework_MockObject_MockObject */ + private $config; + /** @var ITimeFactory | \PHPUnit_Framework_MockObject_MockObject */ + private $timeFactory; + /** @var EnvironmentHelper | \PHPUnit_Framework_MockObject_MockObject */ + private $environmentHelper; + /** @var LostController | \PHPUnit_Framework_MockObject_MockObject */ + private $lostController; + /** @var ResetPassword */ + private $resetPassword; + protected function setUp() { + parent::setUp(); + + $this->userManager = $this->createMock(IUserManager::class); + $this->config = $this->createMock(IConfig::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->environmentHelper = $this->createMock(EnvironmentHelper::class); + $this->lostController = $this->createMock(LostController::class); + $this->resetPassword = new ResetPassword($this->userManager, $this->config, $this->timeFactory, + $this->environmentHelper, $this->lostController); + } + + public function testResetPasswordFromEnv() { + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + + $input->expects($this->once()) + ->method('getArgument') + ->willReturn('foo'); + $input->expects($this->exactly(3)) + ->method('getOption') + ->willReturnMap([ + ['send-email', false], + ['output-link', false], + ['password-from-env', true] + ]); + + $this->environmentHelper->expects($this->once()) + ->method('getEnvVar') + ->willReturn('fooPass'); + + $user = $this->createMock(IUser::class); + $user->expects($this->once()) + ->method('setPassword') + ->willReturn(true); + + $this->userManager->expects($this->once()) + ->method('get') + ->willReturn($user); + + $this->assertNull($this->invokePrivate($this->resetPassword, 'execute', [$input, $output])); + } + + public function testDisplayLink() { + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + + $input->expects($this->once()) + ->method('getArgument') + ->willReturn('foo'); + + $input->expects($this->exactly(3)) + ->method('getOption') + ->willReturnMap([ + ['send-email', false], + ['output-link', true], + ['password-from-env', false] + ]); + + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('foo'); + + $this->userManager->expects($this->once()) + ->method('get') + ->willReturn($user); + + $this->lostController->expects($this->once()) + ->method('generateTokenAndLink') + ->with('foo') + ->willReturn(['http://localhost/foo/bar/123AbcFooBar/foo', '123AbcFooBar']); + + $output->expects($this->once()) + ->method('writeln') + ->with('The password reset link is: http://localhost/foo/bar/123AbcFooBar/foo'); + + $this->invokePrivate($this->resetPassword, 'execute', [$input, $output]); + } + + public function testEmailLink() { + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + + $input->expects($this->once()) + ->method('getArgument') + ->willReturn('foo'); + + $input->expects($this->exactly(3)) + ->method('getOption') + ->willReturnMap([ + ['send-email', true], + ['output-link', false], + ['password-from-env', false] + ]); + + $user = $this->createMock(IUser::class); + + $user->method('getEMailAddress') + ->willReturn('foo@bar.com'); + + $user->method('getUID') + ->willReturn('foo'); + + $this->userManager->expects($this->once()) + ->method('get') + ->willReturn($user); + + $this->lostController->expects($this->once()) + ->method('generateTokenAndLink') + ->with('foo') + ->willReturn(['http://localhost/foo/bar/123AbcFooBar/foo', '123AbcFooBar']); + $this->lostController->expects($this->once()) + ->method('sendEmail') + ->with('foo', '123AbcFooBar', 'http://localhost/foo/bar/123AbcFooBar/foo'); + + $output->expects($this->once()) + ->method('writeln') + ->with('The password reset link is: http://localhost/foo/bar/123AbcFooBar/foo'); + + $this->assertEquals(0, $this->invokePrivate($this->resetPassword, 'execute', [$input, $output])); + } + + public function testEmailLinkFailure() { + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + + $input->expects($this->once()) + ->method('getArgument') + ->willReturn('foo'); + + $input->expects($this->exactly(3)) + ->method('getOption') + ->willReturnMap([ + ['send-email', true], + ['output-link', false], + ['password-from-env', false] + ]); + + $user = $this->createMock(IUser::class); + + $user->expects($this->once()) + ->method('getEMailAddress') + ->willReturn(null); + $user->method('getUID') + ->willReturn('foo'); + + $this->userManager->expects($this->once()) + ->method('get') + ->willReturn($user); + + $output->expects($this->once()) + ->method('writeln') + ->with('Email address is not set for the user foo'); + $this->invokePrivate($this->resetPassword, 'execute', [$input, $output]); + } +}