diff --git a/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php b/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php index c7fd96603f7..eed24215eaa 100644 --- a/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php +++ b/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php @@ -4,6 +4,7 @@ namespace Drupal\Core\Installer\Form; use Drupal\Core\Datetime\TimeZoneFormHelper; use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Extension\ModuleInstallerInterface; use Drupal\Core\Form\ConfigFormBase; use Drupal\Core\Form\FormStateInterface; @@ -67,29 +68,39 @@ class SiteConfigureForm extends ConfigFormBase { * The app root. * @param string $site_path * The site path. - * @param \Drupal\user\UserStorageInterface $user_storage - * The user storage. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface|\Drupal\user\UserStorageInterface $entityTypeManager + * The entity type manager. * @param \Drupal\Core\Extension\ModuleInstallerInterface $module_installer * The module installer. * @param \Drupal\Core\Locale\CountryManagerInterface|\Drupal\user\UserNameValidator $userNameValidator * The user validator. + * @param bool|null $superUserAccessPolicy + * The value of the 'security.enable_super_user' container parameter. */ public function __construct( $root, $site_path, - UserStorageInterface $user_storage, + protected EntityTypeManagerInterface|UserStorageInterface $entityTypeManager, ModuleInstallerInterface $module_installer, protected CountryManagerInterface|UserNameValidator $userNameValidator, + protected ?bool $superUserAccessPolicy = NULL, ) { $this->root = $root; $this->sitePath = $site_path; - $this->userStorage = $user_storage; + if ($this->entityTypeManager instanceof UserStorageInterface) { + @trigger_error('Calling ' . __METHOD__ . '() with the $entityTypeManager argument as UserStorageInterface is deprecated in drupal:10.3.0 and must be EntityTypeManagerInterface in drupal:11.0.0. See https://www.drupal.org/node/3443172', E_USER_DEPRECATED); + $this->entityTypeManager = \Drupal::entityTypeManager(); + } + $this->userStorage = $this->entityTypeManager->getStorage('user'); $this->moduleInstaller = $module_installer; if ($userNameValidator instanceof CountryManagerInterface) { @trigger_error('Calling ' . __METHOD__ . '() with the $userNameValidator argument as CountryManagerInterface is deprecated in drupal:10.3.0 and must be UserNameValidator in drupal:11.0.0. See https://www.drupal.org/node/3431205', E_USER_DEPRECATED); - $userNameValidator = \Drupal::service('user.name_validator'); + $this->userNameValidator = \Drupal::service('user.name_validator'); + } + if ($this->superUserAccessPolicy === NULL) { + @trigger_error('Calling ' . __METHOD__ . '() without the $superUserAccessPolicy argument is deprecated in drupal:10.3.0 and must be passed in drupal:11.0.0. See https://www.drupal.org/node/3443172', E_USER_DEPRECATED); + $this->superUserAccessPolicy = \Drupal::getContainer()->getParameter('security.enable_super_user') ?? TRUE; } - $this->userNameValidator = $userNameValidator; } /** @@ -99,9 +110,13 @@ class SiteConfigureForm extends ConfigFormBase { return new static( $container->getParameter('app.root'), $container->getParameter('site.path'), - $container->get('entity_type.manager')->getStorage('user'), + $container->get('entity_type.manager'), $container->get('module_installer'), $container->get('user.name_validator'), + // In order to disable the super user policy this must be set to FALSE. If + // the container parameter is missing then the policy is enabled. See + // \Drupal\Core\DependencyInjection\Compiler\SuperUserAccessPolicyPass. + $container->getParameter('security.enable_super_user') ?? TRUE, ); } @@ -177,9 +192,16 @@ class SiteConfigureForm extends ConfigFormBase { '#access' => empty($install_state['config_install_path']), ]; + if (count($this->getAdminRoles()) === 0 && $this->superUserAccessPolicy === FALSE) { + $account_label = $this->t('Site account'); + } + else { + $account_label = $this->t('Site maintenance account'); + } + $form['admin_account'] = [ '#type' => 'fieldgroup', - '#title' => $this->t('Site maintenance account'), + '#title' => $account_label, ]; $form['admin_account']['account']['name'] = [ '#type' => 'textfield', @@ -300,6 +322,7 @@ class SiteConfigureForm extends ConfigFormBase { } // We created user 1 with placeholder values. Let's save the real values. + /** @var \Drupal\user\UserInterface $account */ $account = $this->userStorage->load(1); $account->init = $account->mail = $account_values['mail']; $account->roles = $account->getRoles(); @@ -307,7 +330,38 @@ class SiteConfigureForm extends ConfigFormBase { $account->timezone = $form_state->getValue('date_default_timezone'); $account->pass = $account_values['pass']; $account->name = $account_values['name']; + + // Ensure user 1 has an administrator role if one exists. + /** @var \Drupal\user\RoleInterface[] $admin_roles */ + $admin_roles = $this->getAdminRoles(); + if (count(array_intersect($account->getRoles(), array_keys($admin_roles))) === 0) { + if (count($admin_roles) > 0) { + foreach ($admin_roles as $role) { + $account->addRole($role->id()); + } + } + elseif ($this->superUserAccessPolicy === FALSE) { + $this->messenger()->addWarning($this->t( + 'The user %username does not have administrator access. For more information, see the documentation on securing the admin super user.', + [ + '%username' => $account->getDisplayName(), + '@secure-user-1-docs' => 'https://www.drupal.org/docs/administering-a-drupal-site/security-in-drupal/securing-the-admin-super-user-1#s-disable-the-super-user-access-policy', + ] + )); + } + } + $account->save(); } + /** + * Returns the list of admin roles. + * + * @return \Drupal\user\RoleInterface[] + * The list of admin roles. + */ + protected function getAdminRoles(): array { + return $this->entityTypeManager->getStorage('user_role')->loadByProperties(['is_admin' => TRUE]); + } + } diff --git a/core/lib/Drupal/Core/Installer/InstallerAccessPolicy.php b/core/lib/Drupal/Core/Installer/InstallerAccessPolicy.php new file mode 100644 index 00000000000..819578f2a07 --- /dev/null +++ b/core/lib/Drupal/Core/Installer/InstallerAccessPolicy.php @@ -0,0 +1,44 @@ +id()) !== 1 || !InstallerKernel::installationAttempted()) { + return $calculated_permissions; + } + + return $calculated_permissions->addItem(new CalculatedPermissionsItem([], TRUE)); + } + + /** + * {@inheritdoc} + */ + public function getPersistentCacheContexts(): array { + // Note that cache contexts in the installer are ignored because + // \Drupal\Core\Installer\NormalInstallerServiceProvider::register() changes + // everything to use a memory cache. If this was not the case, then this + // should also return a cache context related to the return value of + // \Drupal\Core\Installer\InstallerKernel::installationAttempted(). + return ['user.is_super_user']; + } + +} diff --git a/core/lib/Drupal/Core/Installer/NormalInstallerServiceProvider.php b/core/lib/Drupal/Core/Installer/NormalInstallerServiceProvider.php index a8c6ec84aa4..7c8d8129317 100644 --- a/core/lib/Drupal/Core/Installer/NormalInstallerServiceProvider.php +++ b/core/lib/Drupal/Core/Installer/NormalInstallerServiceProvider.php @@ -12,6 +12,7 @@ use Symfony\Component\DependencyInjection\Compiler\InlineServiceDefinitionsPass; use Symfony\Component\DependencyInjection\Compiler\RemoveUnusedDefinitionsPass; use Symfony\Component\DependencyInjection\Compiler\ReplaceAliasByActualDefinitionPass; use Symfony\Component\DependencyInjection\Compiler\ResolveHotPathPass; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; /** @@ -33,6 +34,9 @@ class NormalInstallerServiceProvider implements ServiceProviderInterface { * {@inheritdoc} */ public function register(ContainerBuilder $container) { + // During the installer user 1 is a superuser. + $container->setDefinition(InstallerAccessPolicy::class, (new Definition())->addTag('access_policy')->setPublic(FALSE)); + // Replace cache services with in-memory implementations. The results in // less queries to set caches which will only be cleared on the next module // install. diff --git a/core/profiles/demo_umami/demo_umami.install b/core/profiles/demo_umami/demo_umami.install index 5c0c94669a6..5404f934a2d 100644 --- a/core/profiles/demo_umami/demo_umami.install +++ b/core/profiles/demo_umami/demo_umami.install @@ -5,7 +5,6 @@ * Install, update and uninstall functions for the demo_umami installation profile. */ -use Drupal\user\Entity\User; use Drupal\shortcut\Entity\Shortcut; /** @@ -34,12 +33,6 @@ function demo_umami_requirements($phase) { * @see system_install() */ function demo_umami_install() { - // Assign user 1 the "administrator" role. - /** @var \Drupal\user\Entity\User $user */ - $user = User::load(1); - $user->addRole('administrator'); - $user->save(); - // We install some menu links, so we have to rebuild the router, to ensure the // menu links are valid. \Drupal::service('router.builder')->rebuildIfNeeded(); diff --git a/core/profiles/standard/standard.install b/core/profiles/standard/standard.install index 22bb1877bd5..6fda4647f17 100644 --- a/core/profiles/standard/standard.install +++ b/core/profiles/standard/standard.install @@ -5,7 +5,6 @@ * Install, update and uninstall functions for the standard installation profile. */ -use Drupal\user\Entity\User; use Drupal\shortcut\Entity\Shortcut; /** @@ -16,12 +15,6 @@ use Drupal\shortcut\Entity\Shortcut; * @see system_install() */ function standard_install() { - // Assign user 1 the "administrator" role. - /** @var \Drupal\user\Entity\User $user */ - $user = User::load(1); - $user->addRole('administrator'); - $user->save(); - // Populate the default shortcut set. $shortcut = Shortcut::create([ 'shortcut_set' => 'default', diff --git a/core/tests/Drupal/FunctionalTests/Installer/SuperUserAccessInstallTest.php b/core/tests/Drupal/FunctionalTests/Installer/SuperUserAccessInstallTest.php new file mode 100644 index 00000000000..826b73504df --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Installer/SuperUserAccessInstallTest.php @@ -0,0 +1,150 @@ + 'profile', + 'core_version_requirement' => '*', + 'name' => 'Superuser testing profile', + ]; + // File API functions are not available yet. + $path = $this->siteDirectory . '/profiles/superuser'; + mkdir($path, 0777, TRUE); + file_put_contents("$path/superuser.info.yml", Yaml::encode($info)); + + file_put_contents("$path/superuser.install", $this->getProvidedData()['install_code']); + + $services = Yaml::decode(file_get_contents(DRUPAL_ROOT . '/sites/default/default.services.yml')); + $services['parameters']['security.enable_super_user'] = $this->getProvidedData()['super_user_policy']; + file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/services.yml', Yaml::encode($services)); + } + + /** + * {@inheritdoc} + */ + protected function setUpSite() { + if ($this->getProvidedData()['super_user_policy'] === FALSE && empty($this->getProvidedData()['expected_roles'])) { + $this->assertSession()->pageTextContains('Site account'); + $this->assertSession()->pageTextNotContains('Site maintenance account'); + } + else { + $this->assertSession()->pageTextNotContains('Site account'); + $this->assertSession()->pageTextContains('Site maintenance account'); + } + parent::setUpSite(); + } + + /** + * Confirms that the installation succeeded. + * + * @dataProvider getInstallTests + */ + public function testInstalled(bool $expected_runtime_has_permission, bool $expected_no_access_message, array $expected_roles): void { + $user = User::load(1); + $this->assertSame($expected_runtime_has_permission, $user->hasPermission('administer software updates')); + $this->assertTrue(\Drupal::state()->get('admin_permission_in_installer')); + $message = sprintf(static::NO_ACCESS_MESSAGE, $this->rootUser->getDisplayName()); + if ($expected_no_access_message) { + $this->assertSession()->pageTextContains($message); + } + else { + $this->assertSession()->pageTextNotContains($message); + } + $this->assertSame($expected_roles, $user->getRoles(TRUE)); + } + + public static function getInstallTests(): array { + $test_cases = []; + $test_cases['runtime super user policy enabled'] = [ + 'expected_runtime_has_permission' => TRUE, + 'expected_no_access_message' => FALSE, + 'expected_roles' => [], + 'install_code' => <<set('admin_permission_in_installer', \$user->hasPermission('administer software updates')); + } + PHP, + 'super_user_policy' => TRUE, + ]; + + $test_cases['no super user policy enabled and no admin role'] = [ + 'expected_runtime_has_permission' => FALSE, + 'expected_no_access_message' => TRUE, + 'expected_roles' => [], + 'install_code' => $test_cases['runtime super user policy enabled']['install_code'], + 'super_user_policy' => FALSE, + ]; + + $test_cases['no super user policy enabled and admin role'] = [ + 'expected_runtime_has_permission' => TRUE, + 'expected_no_access_message' => FALSE, + 'expected_roles' => ['admin_role'], + 'install_code' => <<set('admin_permission_in_installer', \$user->hasPermission('administer software updates')); + \Drupal\user\Entity\Role::create(['id' => 'admin_role', 'label' => 'Admin role'])->setIsAdmin(TRUE)->save(); + \Drupal\user\Entity\Role::create(['id' => 'another_role', 'label' => 'Another role'])->save(); + } + PHP, + 'super_user_policy' => FALSE, + ]; + + $test_cases['no super user policy enabled and multiple admin role'] = [ + 'expected_runtime_has_permission' => TRUE, + 'expected_no_access_message' => FALSE, + 'expected_roles' => ['admin_role', 'another_admin_role'], + 'install_code' => <<set('admin_permission_in_installer', \$user->hasPermission('administer software updates')); + \Drupal\user\Entity\Role::create(['id' => 'admin_role', 'label' => 'Admin role'])->setIsAdmin(TRUE)->save(); + \Drupal\user\Entity\Role::create(['id' => 'another_admin_role', 'label' => 'Another admin role'])->setIsAdmin(TRUE)->save(); + \Drupal\user\Entity\Role::create(['id' => 'another_role', 'label' => 'Another role'])->save(); + } + PHP, + 'super_user_policy' => FALSE, + ]; + + return $test_cases; + } + +}