From c831d57614d07ced23682a6b072b9cc57301e025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Thu, 23 Feb 2023 23:36:40 +0100 Subject: [PATCH] Added an console command to convert local to SAML users and vice versa --- config/services.yaml | 4 + src/Command/User/ConvertToSAMLUserCommand.php | 115 ++++++++++++++++++ src/Command/User/UserListCommand.php | 24 +++- src/Repository/UserRepository.php | 22 ++++ src/Security/SamlUserFactory.php | 4 +- 5 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 src/Command/User/ConvertToSAMLUserCommand.php diff --git a/config/services.yaml b/config/services.yaml index 5b5f1f35..a5914ee2 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -197,6 +197,10 @@ services: arguments: $available_themes: '%partdb.available_themes%' + App\Command\User\ConvertToSAMLUserCommand: + arguments: + $saml_enabled: '%partdb.saml.enabled%' + #################################################################################################################### # Label system diff --git a/src/Command/User/ConvertToSAMLUserCommand.php b/src/Command/User/ConvertToSAMLUserCommand.php new file mode 100644 index 00000000..df48ce06 --- /dev/null +++ b/src/Command/User/ConvertToSAMLUserCommand.php @@ -0,0 +1,115 @@ +. + */ + +namespace App\Command\User; + +use App\Entity\UserSystem\User; +use App\Security\SamlUserFactory; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\Console\Command\Command; +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\Style\SymfonyStyle; + +class ConvertToSAMLUserCommand extends Command +{ + protected static $defaultName = 'partdb:user:convert-to-saml-user|partdb:users:convert-to-saml-user'; + + protected EntityManagerInterface $entityManager; + protected bool $saml_enabled; + + public function __construct(EntityManagerInterface $entityManager, bool $saml_enabled) + { + parent::__construct(); + $this->entityManager = $entityManager; + $this->saml_enabled = $saml_enabled; + } + + protected function configure(): void + { + $this + ->setDescription('Converts a local user to a SAML user (and vice versa)') + ->setHelp('This converts a local user, which can login via the login form, to a SAML user, which can only login via SAML. This is useful if you want to migrate from a local user system to a SAML user system.') + ->addArgument('user', InputArgument::REQUIRED, 'The username (or email) of the user') + ->addOption('to-local', null, InputOption::VALUE_NONE, 'Converts a SAML user to a local user') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $user_name = $input->getArgument('user'); + $to_local = $input->getOption('to-local'); + + if (!$this->saml_enabled && !$to_local) { + $io->confirm('SAML login is not configured. You will not be able to login with this user anymore, when SSO is not configured. Do you really want to continue?'); + } + + /** @var User $user */ + $user = $this->entityManager->getRepository(User::class)->findByEmailOrName($user_name); + + if (!$user) { + $io->error('User not found!'); + + return 1; + } + + $io->info('User found: '.$user->getFullName(true) . ': '.$user->getEmail().' [ID: ' . $user->getID() . ']'); + + if ($to_local) { + return $this->toLocal($user, $io); + } + + return $this->toSAML($user, $io); + } + + public function toLocal(User $user, SymfonyStyle $io): int + { + $io->confirm('You are going to convert a SAML user to a local user. This means, that the user can only login via the login form. ' + . 'The permissions and groups settings of the user will remain unchanged. Do you really want to continue?'); + + $user->setSAMLUser(false); + $user->setPassword(SamlUserFactory::SAML_PASSWORD_PLACEHOLDER); + + $this->entityManager->flush(); + + $io->success('User converted to local user! You will need to set a password for this user, before you can login with it.'); + + return 0; + } + + public function toSAML(User $user, SymfonyStyle $io): int + { + $io->confirm('You are going to convert a local user to a SAML user. This means, that the user can only login via SAML afterwards. The password in the DB will be removed. ' + . 'The permissions and groups settings of the user will remain unchanged. Do you really want to continue?'); + + $user->setSAMLUser(true); + $user->setPassword(SamlUserFactory::SAML_PASSWORD_PLACEHOLDER); + + $this->entityManager->flush(); + + $io->success('User converted to SAML user! You can now login with this user via SAML.'); + + return 0; + } + +} \ No newline at end of file diff --git a/src/Command/User/UserListCommand.php b/src/Command/User/UserListCommand.php index 0f0e52b7..66265dd8 100644 --- a/src/Command/User/UserListCommand.php +++ b/src/Command/User/UserListCommand.php @@ -46,22 +46,39 @@ class UserListCommand extends Command $this ->setDescription('Lists all users') ->setHelp('This command lists all users in the database.') + ->addOption('local', 'l', null, 'Only list local users') + ->addOption('saml', 's', null, 'Only list SAML users') ; } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); + $only_local = $input->getOption('local'); + $only_saml = $input->getOption('saml'); - //Get all users from database - $users = $this->entityManager->getRepository(User::class)->findAll(); + if ($only_local && $only_saml) { + $io->error('You can not use --local and --saml at the same time!'); + + return Command::FAILURE; + } + + $repo = $this->entityManager->getRepository(User::class); + + if ($only_local) { + $users = $repo->onlyLocalUsers(); + } elseif ($only_saml) { + $users = $repo->onlySAMLUsers(); + } else { + $users = $repo->findAll(); + } $io->info(sprintf("Found %d users in database.", count($users))); $io->title('Users:'); $table = new Table($output); - $table->setHeaders(['ID', 'Username', 'Name', 'Email', 'Group', 'Login Disabled']); + $table->setHeaders(['ID', 'Username', 'Name', 'Email', 'Group', 'Login Disabled', 'Type']); foreach ($users as $user) { $table->addRow([ @@ -71,6 +88,7 @@ class UserListCommand extends Command $user->getEmail(), $user->getGroup() !== null ? $user->getGroup()->getName() . ' (ID: ' . $user->getGroup()->getID() . ')' : 'No group', $user->isDisabled() ? 'Yes' : 'No', + $user->isSAMLUser() ? 'SAML' : 'Local', ]); } diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index 2d4fea12..b0ccd964 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -89,4 +89,26 @@ final class UserRepository extends NamedDBElementRepository implements PasswordU $this->getEntityManager()->flush(); } } + + /** + * Returns the list of all local users (not SAML users). + * @return User[] + */ + public function onlyLocalUsers(): array + { + return $this->findBy([ + 'saml_user' => false, + ]); + } + + /** + * Returns the list of all SAML users. + * @return User[] + */ + public function onlySAMLUsers(): array + { + return $this->findBy([ + 'saml_user' => true, + ]); + } } diff --git a/src/Security/SamlUserFactory.php b/src/Security/SamlUserFactory.php index d212f78b..fd181133 100644 --- a/src/Security/SamlUserFactory.php +++ b/src/Security/SamlUserFactory.php @@ -26,12 +26,14 @@ use Symfony\Component\Security\Core\User\UserInterface; class SamlUserFactory implements SamlUserFactoryInterface { + public const SAML_PASSWORD_PLACEHOLDER = '!!SAML!!'; + public function createUser($username, array $attributes = []): UserInterface { $user = new User(); $user->setName($username); $user->setNeedPwChange(false); - $user->setPassword('!!SAML!!'); + $user->setPassword(self::SAML_PASSWORD_PLACEHOLDER); //This is a SAML user now! $user->setSamlUser(true);