From eed32112c504d6333232a1d77e8007f2bdfa40b7 Mon Sep 17 00:00:00 2001 From: Lucas Azevedo Date: Wed, 23 Aug 2023 20:15:42 -0300 Subject: feat: Add user:auth-tokens command Signed-off-by: Lucas Azevedo --- core/Command/User/AuthTokens.php | 70 ++++++++++++++++++++++++++++++++++++++++ core/register_command.php | 1 + 2 files changed, 71 insertions(+) create mode 100644 core/Command/User/AuthTokens.php (limited to 'core') diff --git a/core/Command/User/AuthTokens.php b/core/Command/User/AuthTokens.php new file mode 100644 index 00000000000..938efd952a5 --- /dev/null +++ b/core/Command/User/AuthTokens.php @@ -0,0 +1,70 @@ + + * + * @author Lucas Azevedo + * + * @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 . + * + */ +namespace OC\Core\Command\User; + +use OC\Core\Command\Base; +use OC\Authentication\Token\IProvider; +use OC\Authentication\Token\IToken; +use OCP\IUserManager; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class AuthTokens extends Base { + public function __construct( + protected IUserManager $userManager, + protected IProvider $tokenProvider, + ) { + parent::__construct(); + } + + protected function configure() { + parent::configure(); + + $this + ->setName('user:auth-tokens') + ->setDescription('List authentication tokens of an user') + ->addArgument( + 'user', + InputArgument::REQUIRED, + 'User to list auth tokens for' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $user = $this->userManager->get($input->getArgument('user')); + + if (is_null($user)) { + $output->writeln('user not found'); + return 1; + } + + $tokens = $this->tokenProvider->getTokenByUser($user->getUID()); + + $data = array_map(fn (IToken $token) => $token->jsonSerialize(), $tokens); + + $this->writeArrayInOutputFormat($input, $output, $data); + + return 0; + } +} diff --git a/core/register_command.php b/core/register_command.php index c9b6cc99901..c82c076c207 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -193,6 +193,7 @@ if (\OC::$server->getConfig()->getSystemValue('installed', false)) { $application->add(new OC\Core\Command\User\ListCommand(\OC::$server->getUserManager(), \OC::$server->getGroupManager())); $application->add(new OC\Core\Command\User\Info(\OC::$server->getUserManager(), \OC::$server->getGroupManager())); $application->add(new OC\Core\Command\User\AddAppPassword(\OC::$server->get(\OCP\IUserManager::class), \OC::$server->get(\OC\Authentication\Token\IProvider::class), \OC::$server->get(\OCP\Security\ISecureRandom::class), \OC::$server->get(\OCP\EventDispatcher\IEventDispatcher::class))); + $application->add(new OC\Core\Command\User\AuthTokens(\OC::$server->get(\OCP\IUserManager::class), \OC::$server->get(\OC\Authentication\Token\IProvider::class))); $application->add(new OC\Core\Command\Group\Add(\OC::$server->getGroupManager())); $application->add(new OC\Core\Command\Group\Delete(\OC::$server->getGroupManager())); -- cgit v1.2.3 From 651044ce178fffcbe12ef124c27c37322a829d73 Mon Sep 17 00:00:00 2001 From: Lucas Azevedo Date: Wed, 23 Aug 2023 20:16:08 -0300 Subject: feat: Add user:delete-auth-token command Signed-off-by: Lucas Azevedo --- core/Command/User/DeleteAuthToken.php | 56 +++++++++++++++++++++++++++++++++++ core/register_command.php | 1 + 2 files changed, 57 insertions(+) create mode 100644 core/Command/User/DeleteAuthToken.php (limited to 'core') diff --git a/core/Command/User/DeleteAuthToken.php b/core/Command/User/DeleteAuthToken.php new file mode 100644 index 00000000000..eb31efbf05e --- /dev/null +++ b/core/Command/User/DeleteAuthToken.php @@ -0,0 +1,56 @@ + + * + * @author Lucas Azevedo + * + * @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 . + * + */ +namespace OC\Core\Command\User; + +use OC\Core\Command\Base; +use OC\Authentication\Token\IProvider; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class DeleteAuthToken extends Base { + public function __construct( + protected IProvider $tokenProvider, + ) { + parent::__construct(); + } + + protected function configure() { + $this + ->setName('user:delete-auth-token') + ->setDescription('Deletes an authentication token') + ->addArgument( + 'id', + InputArgument::REQUIRED, + 'ID of the auth token to delete' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $token = $this->tokenProvider->getTokenById($input->getArgument('id')); + + $this->tokenProvider->invalidateTokenById($token->getUID(), $token->getId()); + + return 0; + } +} diff --git a/core/register_command.php b/core/register_command.php index c82c076c207..cfd1a6d2aea 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -194,6 +194,7 @@ if (\OC::$server->getConfig()->getSystemValue('installed', false)) { $application->add(new OC\Core\Command\User\Info(\OC::$server->getUserManager(), \OC::$server->getGroupManager())); $application->add(new OC\Core\Command\User\AddAppPassword(\OC::$server->get(\OCP\IUserManager::class), \OC::$server->get(\OC\Authentication\Token\IProvider::class), \OC::$server->get(\OCP\Security\ISecureRandom::class), \OC::$server->get(\OCP\EventDispatcher\IEventDispatcher::class))); $application->add(new OC\Core\Command\User\AuthTokens(\OC::$server->get(\OCP\IUserManager::class), \OC::$server->get(\OC\Authentication\Token\IProvider::class))); + $application->add(new OC\Core\Command\User\DeleteAuthToken(\OC::$server->get(\OC\Authentication\Token\IProvider::class))); $application->add(new OC\Core\Command\Group\Add(\OC::$server->getGroupManager())); $application->add(new OC\Core\Command\Group\Delete(\OC::$server->getGroupManager())); -- cgit v1.2.3 From 7d05d1f604ad10b6bd2065299a983f7b2c514cd6 Mon Sep 17 00:00:00 2001 From: Lucas Azevedo Date: Thu, 24 Aug 2023 00:10:30 -0300 Subject: Add missing return types Signed-off-by: Lucas Azevedo --- core/Command/User/AuthTokens.php | 4 ++-- core/Command/User/DeleteAuthToken.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'core') diff --git a/core/Command/User/AuthTokens.php b/core/Command/User/AuthTokens.php index 938efd952a5..0555cdfeab3 100644 --- a/core/Command/User/AuthTokens.php +++ b/core/Command/User/AuthTokens.php @@ -38,7 +38,7 @@ class AuthTokens extends Base { parent::__construct(); } - protected function configure() { + protected function configure(): void { parent::configure(); $this @@ -61,7 +61,7 @@ class AuthTokens extends Base { $tokens = $this->tokenProvider->getTokenByUser($user->getUID()); - $data = array_map(fn (IToken $token) => $token->jsonSerialize(), $tokens); + $data = array_map(fn (IToken $token): mixed => $token->jsonSerialize(), $tokens); $this->writeArrayInOutputFormat($input, $output, $data); diff --git a/core/Command/User/DeleteAuthToken.php b/core/Command/User/DeleteAuthToken.php index eb31efbf05e..13b63242b79 100644 --- a/core/Command/User/DeleteAuthToken.php +++ b/core/Command/User/DeleteAuthToken.php @@ -35,7 +35,7 @@ class DeleteAuthToken extends Base { parent::__construct(); } - protected function configure() { + protected function configure(): void { $this ->setName('user:delete-auth-token') ->setDescription('Deletes an authentication token') -- cgit v1.2.3 From ca101b2dbef60bd9a56d5832fdee29e147e80519 Mon Sep 17 00:00:00 2001 From: Lucas Azevedo Date: Thu, 24 Aug 2023 11:19:50 -0300 Subject: Filter out sensitive fields in user:auth-tokens PublicKeyToken::jsonSerialize() already explicitly lists allowed fields, we are adding a second guard here to be on the safe side. Signed-off-by: Lucas Azevedo --- core/Command/User/AuthTokens.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'core') diff --git a/core/Command/User/AuthTokens.php b/core/Command/User/AuthTokens.php index 0555cdfeab3..43fa687781e 100644 --- a/core/Command/User/AuthTokens.php +++ b/core/Command/User/AuthTokens.php @@ -61,7 +61,16 @@ class AuthTokens extends Base { $tokens = $this->tokenProvider->getTokenByUser($user->getUID()); - $data = array_map(fn (IToken $token): mixed => $token->jsonSerialize(), $tokens); + $data = array_map(function (IToken $token): mixed { + $filtered = [ + 'password', + 'password_hash', + 'token', + 'public_key', + 'private_key', + ]; + return array_diff_key($token->jsonSerialize(), array_flip($filtered)); + }, $tokens); $this->writeArrayInOutputFormat($input, $output, $data); -- cgit v1.2.3 From f7bf468e22e74be7e00d3ffa2356ce4212f12760 Mon Sep 17 00:00:00 2001 From: Lucas Azevedo Date: Thu, 24 Aug 2023 11:26:27 -0300 Subject: Use autowiring when registering commands Co-authored-by: Joas Schilling <213943+nickvergessen@users.noreply.github.com> Signed-off-by: Lucas Azevedo --- core/register_command.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'core') diff --git a/core/register_command.php b/core/register_command.php index cfd1a6d2aea..f6ffe231c8f 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -193,8 +193,8 @@ if (\OC::$server->getConfig()->getSystemValue('installed', false)) { $application->add(new OC\Core\Command\User\ListCommand(\OC::$server->getUserManager(), \OC::$server->getGroupManager())); $application->add(new OC\Core\Command\User\Info(\OC::$server->getUserManager(), \OC::$server->getGroupManager())); $application->add(new OC\Core\Command\User\AddAppPassword(\OC::$server->get(\OCP\IUserManager::class), \OC::$server->get(\OC\Authentication\Token\IProvider::class), \OC::$server->get(\OCP\Security\ISecureRandom::class), \OC::$server->get(\OCP\EventDispatcher\IEventDispatcher::class))); - $application->add(new OC\Core\Command\User\AuthTokens(\OC::$server->get(\OCP\IUserManager::class), \OC::$server->get(\OC\Authentication\Token\IProvider::class))); - $application->add(new OC\Core\Command\User\DeleteAuthToken(\OC::$server->get(\OC\Authentication\Token\IProvider::class))); + $application->add(\OC::$server->get(\OC\Core\Command\User\AuthTokens::class)); + $application->add(\OC::$server->get(\OC\Core\Command\User\DeleteAuthToken::class)); $application->add(new OC\Core\Command\Group\Add(\OC::$server->getGroupManager())); $application->add(new OC\Core\Command\Group\Delete(\OC::$server->getGroupManager())); -- cgit v1.2.3 From 5af683d2c4d0883ae9038c33078cbb792a0974e7 Mon Sep 17 00:00:00 2001 From: Lucas Azevedo Date: Thu, 24 Aug 2023 11:42:30 -0300 Subject: Namespace user auth token commands Signed-off-by: Lucas Azevedo --- core/Command/User/AddAppPassword.php | 121 -------------------------- core/Command/User/AuthTokens.php | 79 ----------------- core/Command/User/AuthTokens/Add.php | 122 +++++++++++++++++++++++++++ core/Command/User/AuthTokens/Delete.php | 56 ++++++++++++ core/Command/User/AuthTokens/ListCommand.php | 79 +++++++++++++++++ core/Command/User/DeleteAuthToken.php | 56 ------------ core/register_command.php | 6 +- 7 files changed, 260 insertions(+), 259 deletions(-) delete mode 100644 core/Command/User/AddAppPassword.php delete mode 100644 core/Command/User/AuthTokens.php create mode 100644 core/Command/User/AuthTokens/Add.php create mode 100644 core/Command/User/AuthTokens/Delete.php create mode 100644 core/Command/User/AuthTokens/ListCommand.php delete mode 100644 core/Command/User/DeleteAuthToken.php (limited to 'core') diff --git a/core/Command/User/AddAppPassword.php b/core/Command/User/AddAppPassword.php deleted file mode 100644 index 8c506c8510e..00000000000 --- a/core/Command/User/AddAppPassword.php +++ /dev/null @@ -1,121 +0,0 @@ - - * @author Sean Molenaar - * - * @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 . - * - */ -namespace OC\Core\Command\User; - -use OC\Authentication\Events\AppPasswordCreatedEvent; -use OC\Authentication\Token\IProvider; -use OC\Authentication\Token\IToken; -use OCP\EventDispatcher\IEventDispatcher; -use OCP\IUserManager; -use OCP\Security\ISecureRandom; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Helper\QuestionHelper; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Question\Question; - -class AddAppPassword extends Command { - public function __construct( - protected IUserManager $userManager, - protected IProvider $tokenProvider, - private ISecureRandom $random, - private IEventDispatcher $eventDispatcher, - ) { - parent::__construct(); - } - - protected function configure() { - $this - ->setName('user:add-app-password') - ->setDescription('Add app password for the named user') - ->addArgument( - 'user', - InputArgument::REQUIRED, - 'Username to add app password for' - ) - ->addOption( - 'password-from-env', - null, - InputOption::VALUE_NONE, - 'Read password from environment variable NC_PASS/OC_PASS. Alternatively it will be asked for interactively or an app password without the login password will be created.' - ) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $username = $input->getArgument('user'); - $password = null; - - $user = $this->userManager->get($username); - if (is_null($user)) { - $output->writeln('User does not exist'); - return 1; - } - - if ($input->getOption('password-from-env')) { - $password = getenv('NC_PASS') ?? getenv('OC_PASS'); - if (!$password) { - $output->writeln('--password-from-env given, but NC_PASS is empty!'); - return 1; - } - } elseif ($input->isInteractive()) { - /** @var QuestionHelper $helper */ - $helper = $this->getHelper('question'); - - $question = new Question('Enter the user password: '); - $question->setHidden(true); - /** @var null|string $password */ - $password = $helper->ask($input, $output, $question); - } - - if ($password === null) { - $output->writeln('No password provided. The generated app password will therefore have limited capabilities. Any operation that requires the login password will fail.'); - } - - $token = $this->random->generate(72, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS); - $generatedToken = $this->tokenProvider->generateToken( - $token, - $user->getUID(), - $user->getUID(), - $password, - 'cli', - IToken::PERMANENT_TOKEN, - IToken::DO_NOT_REMEMBER - ); - - $this->eventDispatcher->dispatchTyped( - new AppPasswordCreatedEvent($generatedToken) - ); - - $output->writeln('app password:'); - $output->writeln($token); - - return 0; - } -} diff --git a/core/Command/User/AuthTokens.php b/core/Command/User/AuthTokens.php deleted file mode 100644 index 43fa687781e..00000000000 --- a/core/Command/User/AuthTokens.php +++ /dev/null @@ -1,79 +0,0 @@ - - * - * @author Lucas Azevedo - * - * @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 . - * - */ -namespace OC\Core\Command\User; - -use OC\Core\Command\Base; -use OC\Authentication\Token\IProvider; -use OC\Authentication\Token\IToken; -use OCP\IUserManager; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -class AuthTokens extends Base { - public function __construct( - protected IUserManager $userManager, - protected IProvider $tokenProvider, - ) { - parent::__construct(); - } - - protected function configure(): void { - parent::configure(); - - $this - ->setName('user:auth-tokens') - ->setDescription('List authentication tokens of an user') - ->addArgument( - 'user', - InputArgument::REQUIRED, - 'User to list auth tokens for' - ); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $user = $this->userManager->get($input->getArgument('user')); - - if (is_null($user)) { - $output->writeln('user not found'); - return 1; - } - - $tokens = $this->tokenProvider->getTokenByUser($user->getUID()); - - $data = array_map(function (IToken $token): mixed { - $filtered = [ - 'password', - 'password_hash', - 'token', - 'public_key', - 'private_key', - ]; - return array_diff_key($token->jsonSerialize(), array_flip($filtered)); - }, $tokens); - - $this->writeArrayInOutputFormat($input, $output, $data); - - return 0; - } -} diff --git a/core/Command/User/AuthTokens/Add.php b/core/Command/User/AuthTokens/Add.php new file mode 100644 index 00000000000..e067c069c79 --- /dev/null +++ b/core/Command/User/AuthTokens/Add.php @@ -0,0 +1,122 @@ + + * @author Sean Molenaar + * + * @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 . + * + */ +namespace OC\Core\Command\User\AuthTokens; + +use OC\Authentication\Events\AppPasswordCreatedEvent; +use OC\Authentication\Token\IProvider; +use OC\Authentication\Token\IToken; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IUserManager; +use OCP\Security\ISecureRandom; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\Question; + +class Add extends Command { + public function __construct( + protected IUserManager $userManager, + protected IProvider $tokenProvider, + private ISecureRandom $random, + private IEventDispatcher $eventDispatcher, + ) { + parent::__construct(); + } + + protected function configure() { + $this + ->setName('user:auth-tokens:add') + ->setAliases(['user:add-app-password']) + ->setDescription('Add app password for the named user') + ->addArgument( + 'user', + InputArgument::REQUIRED, + 'Username to add app password for' + ) + ->addOption( + 'password-from-env', + null, + InputOption::VALUE_NONE, + 'Read password from environment variable NC_PASS/OC_PASS. Alternatively it will be asked for interactively or an app password without the login password will be created.' + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $username = $input->getArgument('user'); + $password = null; + + $user = $this->userManager->get($username); + if (is_null($user)) { + $output->writeln('User does not exist'); + return 1; + } + + if ($input->getOption('password-from-env')) { + $password = getenv('NC_PASS') ?? getenv('OC_PASS'); + if (!$password) { + $output->writeln('--password-from-env given, but NC_PASS is empty!'); + return 1; + } + } elseif ($input->isInteractive()) { + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + + $question = new Question('Enter the user password: '); + $question->setHidden(true); + /** @var null|string $password */ + $password = $helper->ask($input, $output, $question); + } + + if ($password === null) { + $output->writeln('No password provided. The generated app password will therefore have limited capabilities. Any operation that requires the login password will fail.'); + } + + $token = $this->random->generate(72, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS); + $generatedToken = $this->tokenProvider->generateToken( + $token, + $user->getUID(), + $user->getUID(), + $password, + 'cli', + IToken::PERMANENT_TOKEN, + IToken::DO_NOT_REMEMBER + ); + + $this->eventDispatcher->dispatchTyped( + new AppPasswordCreatedEvent($generatedToken) + ); + + $output->writeln('app password:'); + $output->writeln($token); + + return 0; + } +} diff --git a/core/Command/User/AuthTokens/Delete.php b/core/Command/User/AuthTokens/Delete.php new file mode 100644 index 00000000000..928387f1cc6 --- /dev/null +++ b/core/Command/User/AuthTokens/Delete.php @@ -0,0 +1,56 @@ + + * + * @author Lucas Azevedo + * + * @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 . + * + */ +namespace OC\Core\Command\User\AuthTokens; + +use OC\Core\Command\Base; +use OC\Authentication\Token\IProvider; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Delete extends Base { + public function __construct( + protected IProvider $tokenProvider, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('user:auth-tokens:delete') + ->setDescription('Deletes an authentication token') + ->addArgument( + 'id', + InputArgument::REQUIRED, + 'ID of the auth token to delete' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $token = $this->tokenProvider->getTokenById($input->getArgument('id')); + + $this->tokenProvider->invalidateTokenById($token->getUID(), $token->getId()); + + return 0; + } +} diff --git a/core/Command/User/AuthTokens/ListCommand.php b/core/Command/User/AuthTokens/ListCommand.php new file mode 100644 index 00000000000..6739e8b4648 --- /dev/null +++ b/core/Command/User/AuthTokens/ListCommand.php @@ -0,0 +1,79 @@ + + * + * @author Lucas Azevedo + * + * @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 . + * + */ +namespace OC\Core\Command\User\AuthTokens; + +use OC\Core\Command\Base; +use OC\Authentication\Token\IProvider; +use OC\Authentication\Token\IToken; +use OCP\IUserManager; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class ListCommand extends Base { + public function __construct( + protected IUserManager $userManager, + protected IProvider $tokenProvider, + ) { + parent::__construct(); + } + + protected function configure(): void { + parent::configure(); + + $this + ->setName('user:auth-tokens:list') + ->setDescription('List authentication tokens of an user') + ->addArgument( + 'user', + InputArgument::REQUIRED, + 'User to list auth tokens for' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $user = $this->userManager->get($input->getArgument('user')); + + if (is_null($user)) { + $output->writeln('user not found'); + return 1; + } + + $tokens = $this->tokenProvider->getTokenByUser($user->getUID()); + + $data = array_map(function (IToken $token): mixed { + $filtered = [ + 'password', + 'password_hash', + 'token', + 'public_key', + 'private_key', + ]; + return array_diff_key($token->jsonSerialize(), array_flip($filtered)); + }, $tokens); + + $this->writeArrayInOutputFormat($input, $output, $data); + + return 0; + } +} diff --git a/core/Command/User/DeleteAuthToken.php b/core/Command/User/DeleteAuthToken.php deleted file mode 100644 index 13b63242b79..00000000000 --- a/core/Command/User/DeleteAuthToken.php +++ /dev/null @@ -1,56 +0,0 @@ - - * - * @author Lucas Azevedo - * - * @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 . - * - */ -namespace OC\Core\Command\User; - -use OC\Core\Command\Base; -use OC\Authentication\Token\IProvider; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -class DeleteAuthToken extends Base { - public function __construct( - protected IProvider $tokenProvider, - ) { - parent::__construct(); - } - - protected function configure(): void { - $this - ->setName('user:delete-auth-token') - ->setDescription('Deletes an authentication token') - ->addArgument( - 'id', - InputArgument::REQUIRED, - 'ID of the auth token to delete' - ); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $token = $this->tokenProvider->getTokenById($input->getArgument('id')); - - $this->tokenProvider->invalidateTokenById($token->getUID(), $token->getId()); - - return 0; - } -} diff --git a/core/register_command.php b/core/register_command.php index f6ffe231c8f..2da8e2aa186 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -192,9 +192,9 @@ if (\OC::$server->getConfig()->getSystemValue('installed', false)) { $application->add(new OC\Core\Command\User\Setting(\OC::$server->getUserManager(), \OC::$server->getConfig())); $application->add(new OC\Core\Command\User\ListCommand(\OC::$server->getUserManager(), \OC::$server->getGroupManager())); $application->add(new OC\Core\Command\User\Info(\OC::$server->getUserManager(), \OC::$server->getGroupManager())); - $application->add(new OC\Core\Command\User\AddAppPassword(\OC::$server->get(\OCP\IUserManager::class), \OC::$server->get(\OC\Authentication\Token\IProvider::class), \OC::$server->get(\OCP\Security\ISecureRandom::class), \OC::$server->get(\OCP\EventDispatcher\IEventDispatcher::class))); - $application->add(\OC::$server->get(\OC\Core\Command\User\AuthTokens::class)); - $application->add(\OC::$server->get(\OC\Core\Command\User\DeleteAuthToken::class)); + $application->add(\OC::$server->get(\OC\Core\Command\User\AuthTokens\Add::class)); + $application->add(\OC::$server->get(\OC\Core\Command\User\AuthTokens\ListCommand::class)); + $application->add(\OC::$server->get(\OC\Core\Command\User\AuthTokens\Delete::class)); $application->add(new OC\Core\Command\Group\Add(\OC::$server->getGroupManager())); $application->add(new OC\Core\Command\Group\Delete(\OC::$server->getGroupManager())); -- cgit v1.2.3 From fe9b9c1955cb33c5026928a9f753bb6bde6e65ab Mon Sep 17 00:00:00 2001 From: Lucas Azevedo Date: Fri, 25 Aug 2023 02:07:57 -0300 Subject: Add last-used-before option Signed-off-by: Lucas Azevedo --- core/Command/User/AuthTokens/Delete.php | 72 ++++++++++++++++++++-- lib/private/Authentication/Token/IProvider.php | 5 ++ lib/private/Authentication/Token/Manager.php | 4 ++ .../Authentication/Token/PublicKeyTokenMapper.php | 9 +++ .../Token/PublicKeyTokenProvider.php | 6 ++ 5 files changed, 92 insertions(+), 4 deletions(-) (limited to 'core') diff --git a/core/Command/User/AuthTokens/Delete.php b/core/Command/User/AuthTokens/Delete.php index 928387f1cc6..830050c1bb9 100644 --- a/core/Command/User/AuthTokens/Delete.php +++ b/core/Command/User/AuthTokens/Delete.php @@ -22,10 +22,14 @@ */ namespace OC\Core\Command\User\AuthTokens; +use DateTimeImmutable; use OC\Core\Command\Base; use OC\Authentication\Token\IProvider; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Delete extends Base { @@ -40,17 +44,77 @@ class Delete extends Base { ->setName('user:auth-tokens:delete') ->setDescription('Deletes an authentication token') ->addArgument( - 'id', + 'uid', InputArgument::REQUIRED, + 'ID of the user to delete tokens for' + ) + ->addArgument( + 'id', + InputArgument::OPTIONAL, 'ID of the auth token to delete' + ) + ->addOption( + 'last-used-before', + null, + InputOption::VALUE_REQUIRED, + 'Delete tokens last used before a given date.' ); } protected function execute(InputInterface $input, OutputInterface $output): int { - $token = $this->tokenProvider->getTokenById($input->getArgument('id')); + $uid = $input->getArgument('uid'); + $id = $input->getArgument('id'); + $before = $input->getOption('last-used-before'); + + if ($before) { + if ($id) { + throw new RuntimeException('Option --last-used-before cannot be used with []'); + } + + return $this->deleteLastUsedBefore($uid, $before); + } + + if (!$id) { + throw new RuntimeException('Not enough arguments. Specify the token or use the --last-used-before option.'); + } + return $this->deleteById($uid, $id); + } + + protected function deleteById(string $uid, string $id) { + $this->tokenProvider->invalidateTokenById($uid, $id); + + return Command::SUCCESS; + } + + protected function deleteLastUsedBefore(string $uid, string $before) { + $date = $this->parseDateOption($before); + if (!$date) { + throw new RuntimeException('Invalid date format. Acceptable formats are: ISO8601 (w/o fractions), "YYYY-MM-DD" and Unix time in seconds.'); + } + + $this->tokenProvider->invalidateLastUsedBefore($uid, $date->getTimestamp()); + + return Command::SUCCESS; + } + + /** + * @return \DateTimeImmutable|false + */ + protected function parseDateOption(string $input) { + $date = false; + + // Handle Unix timestamp + if (filter_var($input, FILTER_VALIDATE_INT)) { + return new DateTimeImmutable('@' . $input); + } - $this->tokenProvider->invalidateTokenById($token->getUID(), $token->getId()); + // ISO8601 + $date = DateTimeImmutable::createFromFormat(DateTimeImmutable::ATOM, $input); + if ($date) { + return $date; + } - return 0; + // YYYY-MM-DD + return DateTimeImmutable::createFromFormat('!Y-m-d', $input); } } diff --git a/lib/private/Authentication/Token/IProvider.php b/lib/private/Authentication/Token/IProvider.php index b5af3f3a5ee..a12d3ba34d9 100644 --- a/lib/private/Authentication/Token/IProvider.php +++ b/lib/private/Authentication/Token/IProvider.php @@ -109,6 +109,11 @@ interface IProvider { */ public function invalidateOldTokens(); + /** + * Invalidate (delete) tokens last used before a given date + */ + public function invalidateLastUsedBefore(string $uid, int $before): void; + /** * Save the updated token * diff --git a/lib/private/Authentication/Token/Manager.php b/lib/private/Authentication/Token/Manager.php index 761e799d298..6a1c7d4c1e7 100644 --- a/lib/private/Authentication/Token/Manager.php +++ b/lib/private/Authentication/Token/Manager.php @@ -204,6 +204,10 @@ class Manager implements IProvider, OCPIProvider { $this->publicKeyTokenProvider->invalidateOldTokens(); } + public function invalidateLastUsedBefore(string $uid, int $before): void { + $this->publicKeyTokenProvider->invalidateLastUsedBefore($uid, $before); + } + /** * @param IToken $token * @param string $oldTokenId diff --git a/lib/private/Authentication/Token/PublicKeyTokenMapper.php b/lib/private/Authentication/Token/PublicKeyTokenMapper.php index 8feb275b3b7..f150576a623 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenMapper.php +++ b/lib/private/Authentication/Token/PublicKeyTokenMapper.php @@ -69,6 +69,15 @@ class PublicKeyTokenMapper extends QBMapper { ->execute(); } + public function invalidateLastUsedBefore(string $uid, int $before): int { + $qb = $this->db->getQueryBuilder(); + return $qb->delete($this->tableName) + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) + ->andWhere($qb->expr()->lt('last_activity', $qb->createNamedParameter($before, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) + ->executeStatement(); + } + /** * Get the user UID for the given token * diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php index f5fcd4dcef2..3fb11611076 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenProvider.php +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -273,6 +273,12 @@ class PublicKeyTokenProvider implements IProvider { $this->mapper->invalidateOld($rememberThreshold, IToken::REMEMBER); } + public function invalidateLastUsedBefore(string $uid, int $before): void { + $this->cache->clear(); + + $this->mapper->invalidateLastUsedBefore($uid, $before); + } + public function updateToken(IToken $token) { $this->cache->clear(); -- cgit v1.2.3 From c93b1634d347b9fe343be4785a6d7b7a3757e7ec Mon Sep 17 00:00:00 2001 From: Lucas Azevedo Date: Fri, 25 Aug 2023 10:41:46 -0300 Subject: Fixes from static analysis Co-authored-by: Joas Schilling <213943+nickvergessen@users.noreply.github.com> Signed-off-by: Lucas Azevedo --- core/Command/User/AuthTokens/Delete.php | 6 +++--- lib/private/Authentication/Token/PublicKeyTokenMapper.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'core') diff --git a/core/Command/User/AuthTokens/Delete.php b/core/Command/User/AuthTokens/Delete.php index 830050c1bb9..8cd6f2e6205 100644 --- a/core/Command/User/AuthTokens/Delete.php +++ b/core/Command/User/AuthTokens/Delete.php @@ -63,7 +63,7 @@ class Delete extends Base { protected function execute(InputInterface $input, OutputInterface $output): int { $uid = $input->getArgument('uid'); - $id = $input->getArgument('id'); + $id = (int) $input->getArgument('id'); $before = $input->getOption('last-used-before'); if ($before) { @@ -80,13 +80,13 @@ class Delete extends Base { return $this->deleteById($uid, $id); } - protected function deleteById(string $uid, string $id) { + protected function deleteById(string $uid, int $id): int { $this->tokenProvider->invalidateTokenById($uid, $id); return Command::SUCCESS; } - protected function deleteLastUsedBefore(string $uid, string $before) { + protected function deleteLastUsedBefore(string $uid, string $before): int { $date = $this->parseDateOption($before); if (!$date) { throw new RuntimeException('Invalid date format. Acceptable formats are: ISO8601 (w/o fractions), "YYYY-MM-DD" and Unix time in seconds.'); diff --git a/lib/private/Authentication/Token/PublicKeyTokenMapper.php b/lib/private/Authentication/Token/PublicKeyTokenMapper.php index f150576a623..b04467a509a 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenMapper.php +++ b/lib/private/Authentication/Token/PublicKeyTokenMapper.php @@ -71,11 +71,11 @@ class PublicKeyTokenMapper extends QBMapper { public function invalidateLastUsedBefore(string $uid, int $before): int { $qb = $this->db->getQueryBuilder(); - return $qb->delete($this->tableName) + $qb->delete($this->tableName) ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) ->andWhere($qb->expr()->lt('last_activity', $qb->createNamedParameter($before, IQueryBuilder::PARAM_INT))) - ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) - ->executeStatement(); + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))); + return $query->executeStatement(); } /** -- cgit v1.2.3 From 9c66bf6dc3ca29ac52f6fd83ff0d05edafb917b3 Mon Sep 17 00:00:00 2001 From: Lucas Azevedo Date: Fri, 25 Aug 2023 11:13:34 -0300 Subject: Use table output for list command Signed-off-by: Lucas Azevedo --- core/Command/User/AuthTokens/ListCommand.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'core') diff --git a/core/Command/User/AuthTokens/ListCommand.php b/core/Command/User/AuthTokens/ListCommand.php index 6739e8b4648..97e94f29f92 100644 --- a/core/Command/User/AuthTokens/ListCommand.php +++ b/core/Command/User/AuthTokens/ListCommand.php @@ -61,18 +61,24 @@ class ListCommand extends Base { $tokens = $this->tokenProvider->getTokenByUser($user->getUID()); - $data = array_map(function (IToken $token): mixed { - $filtered = [ + $tokens = array_map(function (IToken $token) use ($input): mixed { + $sensitive = [ 'password', 'password_hash', 'token', 'public_key', 'private_key', ]; - return array_diff_key($token->jsonSerialize(), array_flip($filtered)); + $data = array_diff_key($token->jsonSerialize(), array_flip($sensitive)); + + if ($input->getOption('output') === self::OUTPUT_FORMAT_PLAIN) { + $data['scope'] = implode(', ', array_keys(array_filter($data['scope']))); + } + + return $data; }, $tokens); - $this->writeArrayInOutputFormat($input, $output, $data); + $this->writeTableInOutputFormat($input, $output, $tokens); return 0; } -- cgit v1.2.3 From cc912c3b51be06a7034c397a2b77d7968a28a7bd Mon Sep 17 00:00:00 2001 From: Lucas Azevedo Date: Sun, 27 Aug 2023 23:02:52 -0300 Subject: Format lastActivity and type for plain output Signed-off-by: Lucas Azevedo --- core/Command/User/AuthTokens/ListCommand.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) (limited to 'core') diff --git a/core/Command/User/AuthTokens/ListCommand.php b/core/Command/User/AuthTokens/ListCommand.php index 97e94f29f92..bfc49606259 100644 --- a/core/Command/User/AuthTokens/ListCommand.php +++ b/core/Command/User/AuthTokens/ListCommand.php @@ -72,7 +72,7 @@ class ListCommand extends Base { $data = array_diff_key($token->jsonSerialize(), array_flip($sensitive)); if ($input->getOption('output') === self::OUTPUT_FORMAT_PLAIN) { - $data['scope'] = implode(', ', array_keys(array_filter($data['scope']))); + $data = $this->formatTokenForPlainOutput($data); } return $data; @@ -82,4 +82,19 @@ class ListCommand extends Base { return 0; } + + public function formatTokenForPlainOutput(array $token): array { + $token['scope'] = implode(', ', array_keys(array_filter($token['scope'] ?? []))); + + $token['lastActivity'] = date(DATE_ATOM, $token['lastActivity']); + + $token['type'] = match ($token['type']) { + IToken::TEMPORARY_TOKEN => 'temporary', + IToken::PERMANENT_TOKEN => 'permanent', + IToken::WIPE_TOKEN => 'wipe', + default => $token['type'], + }; + + return $token; + } } -- cgit v1.2.3