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;
+ }
+
+}