‪TYPO3CMS  ‪main
CreateBackendUserCommand.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Symfony\Component\Console\Command\Command;
21 use Symfony\Component\Console\Helper\QuestionHelper;
22 use Symfony\Component\Console\Input\InputInterface;
23 use Symfony\Component\Console\Input\InputOption;
24 use Symfony\Component\Console\Output\OutputInterface;
25 use Symfony\Component\Console\Question\ChoiceQuestion;
26 use Symfony\Component\Console\Question\ConfirmationQuestion;
27 use Symfony\Component\Console\Question\Question;
28 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
38 
42 class ‪CreateBackendUserCommand extends Command
43 {
44  public function ‪__construct(
45  private readonly ‪ConnectionPool $connectionPool,
46  private readonly ConfigurationManager $configurationManager,
47  private readonly ‪LanguageServiceFactory $languageServiceFactory,
48  ) {
49  parent::__construct();
50  }
51 
52  protected function ‪configure()
53  {
54  $this
55  ->addOption(
56  'username',
57  'u',
58  InputOption::VALUE_REQUIRED,
59  'The username of the backend user',
60  )->addOption(
61  'password',
62  'p',
63  InputOption::VALUE_REQUIRED,
64  'The password of the backend user. See security note below.',
65  )->addOption(
66  'email',
67  'e',
68  InputOption::VALUE_REQUIRED,
69  'The email address of the backend user',
70  '',
71  )
72  ->addOption(
73  'groups',
74  'g',
75  InputOption::VALUE_REQUIRED,
76  'Assign given groups to the user'
77  )
78  ->addOption(
79  'admin',
80  'a',
81  InputOption::VALUE_NONE,
82  'Create user with admin privileges'
83  )->addOption(
84  'maintainer',
85  'm',
86  InputOption::VALUE_NONE,
87  'Create user with maintainer privileges',
88  )->setHelp(
89  <<<EOT
90 
91 <fg=green>Create a backend user using environment variables</>
92 
93 Example:
94 -------------------------------------------------
95 TYPO3_BE_USER_NAME=username \
96 TYPO3_BE_USER_EMAIL=admin@example.com \ TYPO3_BE_USER_GROUPS=<comma-separated-list-of-group-ids> \
97 TYPO3_BE_USER_ADMIN=0 \
98 TYPO3_BE_USER_MAINTAINER=0 \
99 ./bin/typo3 backend:user:create --no-interaction
100 -------------------------------------------------
101 <fg=yellow>
102 Variable "TYPO3_BE_USER_PASSWORD" and options "-p" or "--password" can be
103 used to provide a password. Using this can be a security risk since the password
104 may end up in shell history files. Prefer the interactive mode. Additionally,
105 writing a command to shell history can be suppressed by prefixing the command
106 with a space when using `bash` or `zsh`.
107 </>
108 EOT
109  );
110  }
111 
112  protected function ‪execute(InputInterface $input, OutputInterface ‪$output): int
113  {
114  $input->setInteractive(!$input->getOption('no-interaction'));
115 
117  $questionHelper = $this->getHelper('question');
118  $username = $this->‪getUsername($questionHelper, $input, ‪$output);
119  $password = $this->‪getPassword($questionHelper, $input, ‪$output);
120  $email = $this->‪getEmail($questionHelper, $input, ‪$output) ?: '';
121  $maintainer = $this->‪getMaintainer($questionHelper, $input, ‪$output);
122 
123  // If the user is 'maintainer' it is also required to set the 'admin' flag.
124  if ($maintainer) {
125  $admin = true;
126  } else {
127  $admin = $this->‪getAdmin($questionHelper, $input, ‪$output);
128  }
129 
130  // If 'admin' flag was set, this prompt is skipped.
131  // Because this user does already have access to the entire system.
132  if ($admin) {
133  $groups = [];
134  } else {
135  $groups = $this->‪getGroups($questionHelper, $input, ‪$output);
136  }
137 
138  $this->‪createUser($username, $password, $email, $admin, $maintainer, $groups);
139 
140  return Command::SUCCESS;
141  }
142 
143  private function ‪getUsername(QuestionHelper $questionHelper, InputInterface $input, OutputInterface ‪$output): string
144  {
145  // Taking deleted users into account as we want the username to be unique.
146  // So in case a user was deleted and will be restored, this could cause duplicated usernames.
147  $queryBuilder = $this->connectionPool->getConnectionForTable('be_users');
148  $userList = $queryBuilder->select(['username'], 'be_users')->fetchAllAssociative();
149  $usernames = array_map(static function (array $user): string {
150  return $user['username'];
151  }, $userList);
152 
153  $usernameValidator = static function ($username) use ($usernames) {
154  if (empty($username)) {
155  throw new \RuntimeException(
156  'Backend username must not be empty.',
157  1669822315,
158  );
159  }
160 
161  if (in_array($username, $usernames, true)) {
162  throw new \RuntimeException(
163  'The username "' . $username . '" is already taken. Please use another username.',
164  1670797516,
165  );
166  }
167 
168  return $username;
169  };
170 
171  $usernameFromCli = $this->‪getFallbackValueEnvOrOption($input, 'username', 'TYPO3_BE_USER_NAME');
172  if ($usernameFromCli === false && $input->isInteractive()) {
173  $questionUsername = new Question('Enter the backend username of the new account: ');
174  $questionUsername->setValidator($usernameValidator);
175 
176  return $questionHelper->ask($input, ‪$output, $questionUsername);
177  }
178 
179  return $usernameValidator($usernameFromCli);
180  }
181 
182  private function ‪getPassword(QuestionHelper $questionHelper, InputInterface $input, OutputInterface ‪$output): string
183  {
184  $passwordValidator = function ($password) {
185  $passwordValidationErrors = $this->‪getBackendUserPasswordValidationErrors((string)$password);
186  if (!empty($passwordValidationErrors)) {
187  throw new \RuntimeException(
188  'The given password is not secure enough!' . PHP_EOL
189  . ' * ' . implode(PHP_EOL . ' * ', $passwordValidationErrors),
190  1670267532,
191  );
192  }
193 
194  return $password;
195  };
196 
197  $passwordFromCli = $this->‪getFallbackValueEnvOrOption($input, 'password', 'TYPO3_BE_USER_PASSWORD');
198 
199  // Force this question if no password set via cli.
200  // Thus, the user will always be prompted for a password even --no-interaction is set.
201  $currentlyInteractive = $input->isInteractive();
202  $input->setInteractive(true);
203  if ($passwordFromCli === false) {
204  $questionPassword = new Question('Enter a password for the backend user: ');
205  $questionPassword->setHidden(true);
206  $questionPassword->setHiddenFallback(false);
207  $questionPassword->setValidator($passwordValidator);
208 
209  return $questionHelper->ask($input, ‪$output, $questionPassword);
210  }
211  $input->setInteractive($currentlyInteractive);
212 
213  return $passwordValidator($passwordFromCli);
214  }
215 
216  private function ‪getEmail(QuestionHelper $questionHelper, InputInterface $input, OutputInterface ‪$output): string
217  {
218  $emailValidator = static function ($email) {
219  if (!empty($email) && !GeneralUtility::validEmail($email)) {
220  throw new \RuntimeException(
221  'The given email is not valid! Please try again.',
222  1669813635,
223  );
224  }
225 
226  return $email;
227  };
228 
229  $emailFromCli = $this->‪getFallbackValueEnvOrOption($input, 'email', 'TYPO3_BE_USER_EMAIL');
230  if ($emailFromCli === false && $input->isInteractive()) {
231  $questionEmail = new Question('Enter the email for the backend user: ', '');
232  $questionEmail->setValidator($emailValidator);
233 
234  return $questionHelper->ask($input, ‪$output, $questionEmail);
235  }
236 
237  return (string)$emailValidator($emailFromCli);
238  }
239 
240  private function ‪getGroups(QuestionHelper $questionHelper, InputInterface $input, OutputInterface ‪$output): array
241  {
242  $queryBuilder = $this->connectionPool->getConnectionForTable('be_groups');
243  $groupsList = $queryBuilder->select(['uid', 'title'], 'be_groups')->fetchAllAssociative();
244 
245  $groupChoices = [];
246  foreach ($groupsList as $group) {
247  $groupChoices[$group['uid']] = $group['title'];
248  }
249 
250  $groupValidator = static function ($groupList) use ($groupChoices) {
251  $groups = ‪GeneralUtility::intExplode(',', $groupList ?: '');
252  foreach ($groups as $group) {
253  if (!empty($group) && !isset($groupChoices[$group])) {
254  throw new \RuntimeException(
255  'The given group uid "' . $group . '" does not exist.',
256  1670812929,
257  );
258  }
259  }
260 
261  return $groups;
262  };
263 
264  $groupsFromCli = $this->‪getFallbackValueEnvOrOption($input, 'groups', 'TYPO3_BE_USER_GROUPS');
265  if ($groupsFromCli === false && $input->isInteractive()) {
266  if (empty($groupChoices)) {
267  return [];
268  }
269 
270  $questionGroups = new ChoiceQuestion('Select groups the newly created backend user should be assigned to (use comma seperated list for multiple groups): ', $groupChoices);
271  $questionGroups->setMultiselect(true);
272  $questionGroups->setValidator($groupValidator);
273  // Ensure keys are selected and not the values
274  $questionGroups->setAutocompleterValues(array_keys($groupChoices));
275  return $questionHelper->ask($input, ‪$output, $questionGroups);
276  }
277 
278  return $groupValidator($groupsFromCli);
279  }
280 
281  private function ‪getMaintainer(QuestionHelper $questionHelper, InputInterface $input, OutputInterface ‪$output): bool
282  {
283  $maintainerFromCli = $this->‪getFallbackValueEnvOrOption($input, 'maintainer', 'TYPO3_BE_USER_MAINTAINER');
284  if ($maintainerFromCli === false && $input->isInteractive()) {
285  $questionMaintainer = new ConfirmationQuestion('Create user with maintainer privileges [y/n default: n] ? ', false);
286  return (bool)$questionHelper->ask($input, ‪$output, $questionMaintainer);
287  }
288 
289  return (bool)$maintainerFromCli;
290  }
291 
292  private function ‪getAdmin(QuestionHelper $questionHelper, InputInterface $input, OutputInterface ‪$output): bool
293  {
294  $adminFromCli = $this->‪getFallbackValueEnvOrOption($input, 'admin', 'TYPO3_BE_USER_ADMIN');
295  if ($adminFromCli === false && $input->isInteractive()) {
296  $questionAdmin = new ConfirmationQuestion('Create user with admin privileges [y/n default: n] ? ', false);
297  return (bool)$questionHelper->ask($input, ‪$output, $questionAdmin);
298  }
299 
300  return (bool)$adminFromCli;
301  }
302 
308  private function ‪getFallbackValueEnvOrOption(InputInterface $input, string $option, string $envVar): string|bool
309  {
310  $optionShortcut = $this->getDefinition()->getOption($option)->getShortcut();
311  $parameterOptions = ['--' . $option];
312  if ($optionShortcut !== null) {
313  $parameterOptions[] = '-' . $optionShortcut;
314  }
315  return $input->hasParameterOption($parameterOptions) ? $input->getOption($option) : getenv($envVar);
316  }
317 
318  private function ‪getBackendUserPasswordValidationErrors(string $password): array
319  {
320  ‪$GLOBALS['LANG'] = $this->languageServiceFactory->create('default');
321  $passwordPolicy = ‪$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy'] ?? 'default';
322  $passwordPolicyValidator = new ‪PasswordPolicyValidator(
324  is_string($passwordPolicy) ? $passwordPolicy : ''
325  );
326  $contextData = new ‪ContextData();
327  $passwordPolicyValidator->isValidPassword($password, $contextData);
328 
329  return $passwordPolicyValidator->getValidationErrors();
330  }
331 
337  private function ‪createUser(string $username, string $password, string $email = '', bool $admin = false, bool $maintainer = false, array $groups = []): void
338  {
339  // Initialize backend user authentication to ensure the new backend user can be created with proper permissions
341 
342  $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
343  $backendUserId = ‪StringUtility::getUniqueId('NEW');
344  $data = [
345  'be_users' => [
346  $backendUserId => [
347  'pid' => 0,
348  'username' => $username,
349  'password' => $password,
350  'email' => $email,
351  'admin' => $admin ? 1 : 0,
352  'usergroup' => $groups,
353  'disable' => 0,
354  ],
355  ],
356  ];
357 
358  $dataHandler->start($data, []);
359  $dataHandler->process_datamap();
360 
361  $backendUserId = $dataHandler->substNEWwithIDs[$backendUserId] ?? null;
362  if ($maintainer && $backendUserId) {
363  $maintainerIds = $this->configurationManager->getConfigurationValueByPath('SYS/systemMaintainers') ?? [];
364  sort($maintainerIds);
365  $maintainerIds[] = $backendUserId;
366  $this->configurationManager->setLocalConfigurationValuesByPathValuePairs([
367  'SYS/systemMaintainers' => array_unique($maintainerIds),
368  ]);
369  }
370  }
371 }
372 
‪TYPO3\CMS\Core\Localization\LanguageServiceFactory
Definition: LanguageServiceFactory.php:25
‪TYPO3\CMS\Core\DataHandling\DataHandler
Definition: DataHandler.php:94
‪TYPO3\CMS\Backend\Command\CreateBackendUserCommand\getPassword
‪getPassword(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output)
Definition: CreateBackendUserCommand.php:183
‪TYPO3\CMS\Backend\Command\CreateBackendUserCommand\getAdmin
‪getAdmin(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output)
Definition: CreateBackendUserCommand.php:293
‪TYPO3\CMS\Backend\Command\CreateBackendUserCommand\getMaintainer
‪getMaintainer(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output)
Definition: CreateBackendUserCommand.php:282
‪TYPO3\CMS\Backend\Command\CreateBackendUserCommand\getFallbackValueEnvOrOption
‪getFallbackValueEnvOrOption(InputInterface $input, string $option, string $envVar)
Definition: CreateBackendUserCommand.php:309
‪TYPO3\CMS\Backend\Command\CreateBackendUserCommand\__construct
‪__construct(private readonly ConnectionPool $connectionPool, private readonly ConfigurationManager $configurationManager, private readonly LanguageServiceFactory $languageServiceFactory,)
Definition: CreateBackendUserCommand.php:44
‪TYPO3\CMS\Backend\Command\CreateBackendUserCommand\getUsername
‪getUsername(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output)
Definition: CreateBackendUserCommand.php:144
‪TYPO3\CMS\Backend\Command\CreateBackendUserCommand
Definition: CreateBackendUserCommand.php:43
‪TYPO3\CMS\Backend\Command\CreateBackendUserCommand\getBackendUserPasswordValidationErrors
‪getBackendUserPasswordValidationErrors(string $password)
Definition: CreateBackendUserCommand.php:319
‪TYPO3\CMS\Backend\Command
Definition: CreateBackendUserCommand.php:18
‪TYPO3\CMS\Core\PasswordPolicy\NEW_USER_PASSWORD
‪@ NEW_USER_PASSWORD
Definition: PasswordPolicyAction.php:27
‪TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyValidator
Definition: PasswordPolicyValidator.php:27
‪TYPO3\CMS\Backend\Command\CreateBackendUserCommand\createUser
‪createUser(string $username, string $password, string $email='', bool $admin=false, bool $maintainer=false, array $groups=[])
Definition: CreateBackendUserCommand.php:338
‪$output
‪$output
Definition: annotationChecker.php:114
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Core\Bootstrap
Definition: Bootstrap.php:62
‪TYPO3\CMS\Backend\Command\CreateBackendUserCommand\getGroups
‪getGroups(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output)
Definition: CreateBackendUserCommand.php:241
‪TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyAction
‪PasswordPolicyAction
Definition: PasswordPolicyAction.php:24
‪TYPO3\CMS\Backend\Command\CreateBackendUserCommand\execute
‪execute(InputInterface $input, OutputInterface $output)
Definition: CreateBackendUserCommand.php:113
‪TYPO3\CMS\Backend\Command\CreateBackendUserCommand\getEmail
‪getEmail(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output)
Definition: CreateBackendUserCommand.php:217
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Utility\StringUtility
Definition: StringUtility.php:24
‪TYPO3\CMS\Core\Utility\GeneralUtility\intExplode
‪static list< int > intExplode(string $delimiter, string $string, bool $removeEmptyValues=false)
Definition: GeneralUtility.php:756
‪TYPO3\CMS\Core\Core\Bootstrap\initializeBackendAuthentication
‪static initializeBackendAuthentication()
Definition: Bootstrap.php:527
‪TYPO3\CMS\Core\Utility\StringUtility\getUniqueId
‪static getUniqueId(string $prefix='')
Definition: StringUtility.php:57
‪TYPO3\CMS\Backend\Command\CreateBackendUserCommand\configure
‪configure()
Definition: CreateBackendUserCommand.php:52
‪TYPO3\CMS\Core\PasswordPolicy\Validator\Dto\ContextData
Definition: ContextData.php:28