diff --git a/core/Command/User/Add.php b/core/Command/User/Add.php index 1e34b435e930d..3411946fdeb2f 100644 --- a/core/Command/User/Add.php +++ b/core/Command/User/Add.php @@ -2,6 +2,7 @@ /** * @copyright Copyright (c) 2016, ownCloud, Inc. * + * @author Anupam Kumar * @author Arthur Schiwon * @author Christoph Wurst * @author Joas Schilling @@ -26,10 +27,16 @@ namespace OC\Core\Command\User; use OC\Files\Filesystem; +use OCA\Settings\Mailer\NewUserMailHelper; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IAppConfig; use OCP\IGroup; use OCP\IGroupManager; use OCP\IUser; use OCP\IUserManager; +use OCP\Mail\IMailer; +use OCP\Security\Events\GenerateSecurePasswordEvent; +use OCP\Security\ISecureRandom; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; @@ -42,11 +49,16 @@ class Add extends Command { public function __construct( protected IUserManager $userManager, protected IGroupManager $groupManager, + protected IMailer $mailer, + private IAppConfig $appConfig, + private NewUserMailHelper $mailHelper, + private IEventDispatcher $eventDispatcher, + private ISecureRandom $secureRandom, ) { parent::__construct(); } - protected function configure() { + protected function configure(): void { $this ->setName('user:add') ->setDescription('adds an account') @@ -61,6 +73,12 @@ protected function configure() { InputOption::VALUE_NONE, 'read password from environment variable OC_PASS' ) + ->addOption( + 'generate-password', + null, + InputOption::VALUE_NONE, + 'Generate a secure password. A welcome email with a reset link will be sent to the user via an email if --email option and newUser.sendEmail config are set' + ) ->addOption( 'display-name', null, @@ -72,6 +90,12 @@ protected function configure() { 'g', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'groups the account should be added to (The group will be created if it does not exist)' + ) + ->addOption( + 'email', + null, + InputOption::VALUE_REQUIRED, + 'When set, users may register using the default email verification workflow' ); } @@ -82,12 +106,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } + $password = ''; + + // Setup password. if ($input->getOption('password-from-env')) { $password = getenv('OC_PASS'); + if (!$password) { $output->writeln('--password-from-env given, but OC_PASS is empty!'); return 1; } + } elseif ($input->getOption('generate-password')) { + $passwordEvent = new GenerateSecurePasswordEvent(); + $this->eventDispatcher->dispatchTyped($passwordEvent); + $password = $passwordEvent->getPassword() ?? $this->secureRandom->generate(20); } elseif ($input->isInteractive()) { /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); @@ -105,21 +137,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } } else { - $output->writeln("Interactive input or --password-from-env is needed for entering a password!"); + $output->writeln("Interactive input or --password-from-env or --generate-password is needed for setting a password!"); return 1; } try { $user = $this->userManager->createUser( $input->getArgument('uid'), - $password + $password, ); } catch (\Exception $e) { $output->writeln('' . $e->getMessage() . ''); return 1; } - if ($user instanceof IUser) { $output->writeln('The account "' . $user->getUID() . '" was created successfully'); } else { @@ -154,6 +185,30 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln('Account "' . $user->getUID() . '" added to group "' . $group->getGID() . '"'); } } + + $email = $input->getOption('email'); + if (!empty($email)) { + if (!$this->mailer->validateMailAddress($email)) { + $output->writeln(\sprintf( + 'The given email address "%s" is invalid. Email not set for the user.', + $email, + )); + + return 1; + } + + $user->setSystemEMailAddress($email); + + if ($this->appConfig->getValueString('core', 'newUser.sendEmail', 'yes') === 'yes') { + try { + $this->mailHelper->sendMail($user, $this->mailHelper->generateTemplate($user, true)); + $output->writeln('Welcome email sent to ' . $email); + } catch (\Exception $e) { + $output->writeln('Unable to send the welcome email to ' . $email); + } + } + } + return 0; } } diff --git a/tests/Core/Command/User/AddTest.php b/tests/Core/Command/User/AddTest.php new file mode 100644 index 0000000000000..3ca6b635a9467 --- /dev/null +++ b/tests/Core/Command/User/AddTest.php @@ -0,0 +1,170 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +declare(strict_types=1); + +namespace Core\Command\User; + +use OC\Core\Command\User\Add; +use OCA\Settings\Mailer\NewUserMailHelper; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IAppConfig; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Mail\IEMailTemplate; +use OCP\mail\IMailer; +use OCP\Security\ISecureRandom; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Test\TestCase; + +class AddTest extends TestCase { + /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */ + private $userManager; + + /** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */ + private $groupManager; + + /** @var IMailer|\PHPUnit\Framework\MockObject\MockObject */ + private $mailer; + + /** @var IAppConfig|\PHPUnit\Framework\MockObject\MockObject */ + private $appConfig; + + /** @var NewUserMailHelper|\PHPUnit\Framework\MockObject\MockObject */ + private $mailHelper; + + /** @var IEventDispatcher|\PHPUnit\Framework\MockObject\MockObject */ + private $eventDispatcher; + + /** @var ISecureRandom|\PHPUnit\Framework\MockObject\MockObject */ + private $secureRandom; + + /** @var IUser|\PHPUnit\Framework\MockObject\MockObject */ + private $user; + + /** @var InputInterface|\PHPUnit\Framework\MockObject\MockObject */ + private $consoleInput; + + /** @var OutputInterface|\PHPUnit\Framework\MockObject\MockObject */ + private $consoleOutput; + + /** @var Add */ + private $addCommand; + + public function setUp(): void { + parent::setUp(); + + $this->userManager = static::createMock(IUserManager::class); + $this->groupManager = static::createStub(IGroupManager::class); + $this->mailer = static::createMock(IMailer::class); + $this->appConfig = static::createMock(IAppConfig::class); + $this->mailHelper = static::createMock(NewUserMailHelper::class); + $this->eventDispatcher = static::createStub(IEventDispatcher::class); + $this->secureRandom = static::createStub(ISecureRandom::class); + + $this->user = static::createMock(IUser::class); + + $this->consoleInput = static::createMock(InputInterface::class); + $this->consoleOutput = static::createMock(OutputInterface::class); + + $this->addCommand = new Add( + $this->userManager, + $this->groupManager, + $this->mailer, + $this->appConfig, + $this->mailHelper, + $this->eventDispatcher, + $this->secureRandom + ); + } + + /** + * @dataProvider addEmailDataProvider + */ + public function testAddEmail( + ?string $email, + bool $isEmailValid, + bool $shouldSendEmail, + ): void { + $this->user->expects($isEmailValid ? static::once() : static::never()) + ->method('setSystemEMailAddress') + ->with(static::equalTo($email)); + + $this->userManager->method('createUser') + ->willReturn($this->user); + + $this->appConfig->method('getValueString') + ->willReturn($shouldSendEmail ? 'yes' : 'no'); + + $this->mailer->method('validateMailAddress') + ->willReturn($isEmailValid); + + $this->mailHelper->method('generateTemplate') + ->willReturn(static::createMock(IEMailTemplate::class)); + + $this->mailHelper->expects($isEmailValid && $shouldSendEmail ? static::once() : static::never()) + ->method('sendMail'); + + $this->consoleInput->method('getOption') + ->will(static::returnValueMap([ + ['generate-password', 'true'], + ['email', $email], + ['group', []], + ])); + + $this->invokePrivate($this->addCommand, 'execute', [ + $this->consoleInput, + $this->consoleOutput + ]); + } + + /** + * @return array + */ + public function addEmailDataProvider(): array { + return [ + 'Valid E-Mail' => [ + 'info@example.com', + true, + true, + ], + 'Invalid E-Mail' => [ + 'info@@example.com', + false, + false, + ], + 'No E-Mail' => [ + '', + false, + false, + ], + 'Valid E-Mail, but no mail should be sent' => [ + 'info@example.com', + true, + false, + ], + ]; + } +}