addOption( 'username', 'u', InputOption::VALUE_REQUIRED, 'The username of the backend user', )->addOption( 'password', 'p', InputOption::VALUE_REQUIRED, 'The password of the backend user. See security note below.', )->addOption( 'email', 'e', InputOption::VALUE_REQUIRED, 'The email address of the backend user', '', ) ->addOption( 'groups', 'g', InputOption::VALUE_REQUIRED, 'Assign given groups to the user' ) ->addOption( 'language', 'l', InputOption::VALUE_REQUIRED, 'The language for the user interface' ) ->addOption( 'admin', 'a', InputOption::VALUE_NONE, 'Create user with admin privileges' )->addOption( 'maintainer', 'm', InputOption::VALUE_NONE, 'Create user with maintainer privileges', )->setHelp( <<Create a backend user using environment variables Example: ------------------------------------------------- TYPO3_BE_USER_NAME=username \ TYPO3_BE_USER_EMAIL=admin@example.com \ TYPO3_BE_USER_GROUPS= \ TYPO3_BE_USER_LANGUAGE=de \ TYPO3_BE_USER_ADMIN=0 \ TYPO3_BE_USER_MAINTAINER=0 \ ./bin/typo3 backend:user:create --no-interaction ------------------------------------------------- Variable "TYPO3_BE_USER_PASSWORD" and options "-p" or "--password" can be used to provide a password. Using this can be a security risk since the password may end up in shell history files. Prefer the interactive mode. Additionally, writing a command to shell history can be suppressed by prefixing the command with a space when using `bash` or `zsh`. EOT ); } protected function execute(InputInterface $input, OutputInterface $output): int { $input->setInteractive(!$input->getOption('no-interaction')); /** @var QuestionHelper $questionHelper */ $questionHelper = $this->getHelper('question'); $username = $this->getUsername($questionHelper, $input, $output); $password = $this->getPassword($questionHelper, $input, $output); $email = $this->getEmail($questionHelper, $input, $output) ?: ''; $maintainer = $this->getMaintainer($questionHelper, $input, $output); $language = $this->getLanguage($questionHelper, $input, $output) ?: 'en'; // If the user is 'maintainer' it is also required to set the 'admin' flag. if ($maintainer) { $admin = true; } else { $admin = $this->getAdmin($questionHelper, $input, $output); } // If 'admin' flag was set, this prompt is skipped. // Because this user does already have access to the entire system. if ($admin) { $groups = []; } else { $groups = $this->getGroups($questionHelper, $input, $output); } $this->createUser($username, $password, $email, $admin, $maintainer, $groups, $language); return Command::SUCCESS; } private function getUsername(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): string { // Taking deleted users into account as we want the username to be unique. // So in case a user was deleted and will be restored, this could cause duplicated usernames. $queryBuilder = $this->connectionPool->getQueryBuilderForTable('be_users'); $queryBuilder->getRestrictions()->removeAll(); $usernames = $queryBuilder ->select('username') ->from('be_users') ->executeQuery() ->fetchFirstColumn(); $usernameValidator = static function ($username) use ($usernames) { if (empty($username)) { throw new \RuntimeException( 'Backend username must not be empty.', 1669822315, ); } if (in_array($username, $usernames, true)) { throw new \RuntimeException( 'The username "' . $username . '" is already taken. Please use another username.', 1670797516, ); } return $username; }; $usernameFromCli = $this->getFallbackValueEnvOrOption($input, 'username', 'TYPO3_BE_USER_NAME'); if ($usernameFromCli === false && $input->isInteractive()) { $questionUsername = new Question('Enter the backend username of the new account: '); $questionUsername->setValidator($usernameValidator); return $questionHelper->ask($input, $output, $questionUsername); } return $usernameValidator($usernameFromCli); } private function getPassword(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): string { $passwordValidator = function ($password) { $passwordValidationErrors = $this->getBackendUserPasswordValidationErrors((string)$password); if (!empty($passwordValidationErrors)) { throw new \RuntimeException( 'The given password is not secure enough!' . PHP_EOL . ' * ' . implode(PHP_EOL . ' * ', $passwordValidationErrors), 1670267532, ); } return $password; }; $passwordFromCli = $this->getFallbackValueEnvOrOption($input, 'password', 'TYPO3_BE_USER_PASSWORD'); // Force this question if no password set via cli. // Thus, the user will always be prompted for a password even --no-interaction is set. $currentlyInteractive = $input->isInteractive(); $input->setInteractive(true); if ($passwordFromCli === false) { $questionPassword = new Question('Enter a password for the backend user: '); $questionPassword->setHidden(true); $questionPassword->setHiddenFallback(false); $questionPassword->setValidator($passwordValidator); return $questionHelper->ask($input, $output, $questionPassword); } $input->setInteractive($currentlyInteractive); return $passwordValidator($passwordFromCli); } private function getEmail(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): string { $emailValidator = static function ($email) { if (!empty($email) && !GeneralUtility::validEmail($email)) { throw new \RuntimeException( 'The given email is not valid! Please try again.', 1669813635, ); } return $email; }; $emailFromCli = $this->getFallbackValueEnvOrOption($input, 'email', 'TYPO3_BE_USER_EMAIL'); if ($emailFromCli === false && $input->isInteractive()) { $questionEmail = new Question('Enter the email for the backend user: ', ''); $questionEmail->setValidator($emailValidator); return $questionHelper->ask($input, $output, $questionEmail); } return (string)$emailValidator($emailFromCli); } private function getGroups(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): array { $queryBuilder = $this->connectionPool->getConnectionForTable('be_groups'); $groupsList = $queryBuilder->select(['uid', 'title'], 'be_groups')->fetchAllAssociative(); $groupChoices = []; foreach ($groupsList as $group) { $groupChoices[$group['uid']] = $group['title']; } $groupValidator = static function ($groupList) use ($groupChoices) { $groups = GeneralUtility::intExplode(',', $groupList ?: ''); foreach ($groups as $group) { if (!empty($group) && !isset($groupChoices[$group])) { throw new \RuntimeException( 'The given group uid "' . $group . '" does not exist.', 1670812929, ); } } return $groups; }; $groupsFromCli = $this->getFallbackValueEnvOrOption($input, 'groups', 'TYPO3_BE_USER_GROUPS'); if ($groupsFromCli === false && $input->isInteractive()) { if (empty($groupChoices)) { return []; } $questionGroups = new ChoiceQuestion('Select groups the newly created backend user should be assigned to (use comma-separated list for multiple groups): ', $groupChoices); $questionGroups->setMultiselect(true); $questionGroups->setValidator($groupValidator); // Ensure keys are selected and not the values $questionGroups->setAutocompleterValues(array_keys($groupChoices)); return $questionHelper->ask($input, $output, $questionGroups); } return $groupValidator($groupsFromCli); } private function getMaintainer(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): bool { $maintainerFromCli = $this->getFallbackValueEnvOrOption($input, 'maintainer', 'TYPO3_BE_USER_MAINTAINER'); if ($maintainerFromCli === false && $input->isInteractive()) { $questionMaintainer = new ConfirmationQuestion('Create user with maintainer privileges [y/n default: n] ? ', false); return (bool)$questionHelper->ask($input, $output, $questionMaintainer); } return (bool)$maintainerFromCli; } private function getAdmin(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): bool { $adminFromCli = $this->getFallbackValueEnvOrOption($input, 'admin', 'TYPO3_BE_USER_ADMIN'); if ($adminFromCli === false && $input->isInteractive()) { $questionAdmin = new ConfirmationQuestion('Create user with admin privileges [y/n default: n] ? ', false); return (bool)$questionHelper->ask($input, $output, $questionAdmin); } return (bool)$adminFromCli; } private function getLanguage(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): string { $languagesList = $this->locales->getLanguages(); $languageValidator = static function ($language) use ($languagesList) { if (!empty($language) && !isset($languagesList[$language])) { throw new \RuntimeException( 'The given language "' . $language . '" is not supported.', 1769429507 ); } return $language; }; $languageFromCli = $this->getFallbackValueEnvOrOption($input, 'language', 'TYPO3_BE_USER_LANGUAGE'); if ($languageFromCli === false && $input->isInteractive()) { $questionLanguage = new Question('Enter the language for the user interface [eg: de/fr/it/...]: ', ''); $questionLanguage->setValidator($languageValidator); $questionLanguage->setAutocompleterValues(array_keys($languagesList)); return $questionHelper->ask($input, $output, $questionLanguage); } return (string)$languageValidator($languageFromCli); } /** * Get a value from * 1. environment variable * 2. cli option */ private function getFallbackValueEnvOrOption(InputInterface $input, string $option, string $envVar): string|bool { $optionShortcut = $this->getDefinition()->getOption($option)->getShortcut(); $parameterOptions = ['--' . $option]; if ($optionShortcut !== null) { $parameterOptions[] = '-' . $optionShortcut; } return $input->hasParameterOption($parameterOptions) ? $input->getOption($option) : getenv($envVar); } private function getBackendUserPasswordValidationErrors(string $password): array { $GLOBALS['LANG'] = $this->languageServiceFactory->create('en'); $passwordPolicy = $GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy'] ?? 'default'; $passwordPolicyValidator = new PasswordPolicyValidator( PasswordPolicyAction::NEW_USER_PASSWORD, is_string($passwordPolicy) ? $passwordPolicy : '' ); $contextData = new ContextData(); $passwordPolicyValidator->isValidPassword($password, $contextData); return $passwordPolicyValidator->getValidationErrors(); } /** * Create a backend user. * similar to "\TYPO3\CMS\Install\Service\SetupService::createUser()", * but accepts admin/maintainer flag and groups */ private function createUser(string $username, string $password, string $email = '', bool $admin = false, bool $maintainer = false, array $groups = [], string $language = 'en'): void { // Initialize backend user authentication to ensure the new backend user can be created with proper permissions Bootstrap::initializeBackendAuthentication(); $dataHandler = GeneralUtility::makeInstance(DataHandler::class); $backendUserId = StringUtility::getUniqueId('NEW'); $data = [ 'be_users' => [ $backendUserId => [ 'pid' => 0, 'username' => $username, 'password' => $password, 'email' => $email, 'admin' => $admin ? 1 : 0, 'usergroup' => $groups, 'disable' => 0, 'lang' => $language, ], ], ]; $dataHandler->start($data, []); $dataHandler->process_datamap(); $backendUserId = $dataHandler->substNEWwithIDs[$backendUserId] ?? null; if ($maintainer && $backendUserId) { $maintainerIds = $this->configurationManager->getConfigurationValueByPath('SYS/systemMaintainers') ?? []; sort($maintainerIds); $maintainerIds[] = $backendUserId; $this->configurationManager->setLocalConfigurationValuesByPathValuePairs([ 'SYS/systemMaintainers' => array_unique($maintainerIds), ]); } } }