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