diff --git a/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php b/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php
index 7fe4b98d03..7a2f13fb3a 100644
--- a/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php
+++ b/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php
@@ -234,7 +234,7 @@ public function testCacheabilityOf401Response() {
// If the permissions of the 'anonymous' role change, it may no longer be
// necessary to be authenticated to access this route. Therefore the cached
// 401 responses should be invalidated.
- $this->grantPermissions(Role::load(Role::ANONYMOUS_ID), [$this->randomMachineName()]);
+ $this->grantPermissions(Role::load(Role::ANONYMOUS_ID), ['access content']);
$assert_response_cacheability('MISS', 'MISS');
$assert_response_cacheability('HIT', 'MISS');
// Idem for when the 'system.site' config changes.
diff --git a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockContentTranslationTest.php b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockContentTranslationTest.php
index 4005419b90..f9673cde76 100644
--- a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockContentTranslationTest.php
+++ b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockContentTranslationTest.php
@@ -24,6 +24,7 @@ class MigrateBlockContentTranslationTest extends MigrateDrupal6TestBase {
'block_content',
'config_translation',
'language',
+ 'locale',
'path_alias',
'statistics',
'taxonomy',
diff --git a/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockContentTranslationTest.php b/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockContentTranslationTest.php
index d008562f6b..9f3a5c77a0 100644
--- a/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockContentTranslationTest.php
+++ b/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockContentTranslationTest.php
@@ -27,6 +27,7 @@ class MigrateBlockContentTranslationTest extends MigrateDrupal7TestBase {
'block_content',
'config_translation',
'language',
+ 'locale',
'path_alias',
'statistics',
'taxonomy',
diff --git a/core/modules/config/tests/src/Functional/ConfigInstallProfileOverrideTest.php b/core/modules/config/tests/src/Functional/ConfigInstallProfileOverrideTest.php
index e8b9afa1ce..4b723d3b4e 100644
--- a/core/modules/config/tests/src/Functional/ConfigInstallProfileOverrideTest.php
+++ b/core/modules/config/tests/src/Functional/ConfigInstallProfileOverrideTest.php
@@ -141,6 +141,7 @@ public function testInstallProfileConfigOverwrite() {
// Ensure the authenticated role has the access tour permission.
$role = Role::load(Role::AUTHENTICATED_ID);
$this->assertTrue($role->hasPermission('access tour'), 'The Authenticated role has the "access tour" permission.');
+ $this->assertEquals(['module' => ['tour']], $role->getDependencies());
}
}
diff --git a/core/modules/content_moderation/src/Permissions.php b/core/modules/content_moderation/src/Permissions.php
index af0e104a7c..68639faf30 100644
--- a/core/modules/content_moderation/src/Permissions.php
+++ b/core/modules/content_moderation/src/Permissions.php
@@ -39,6 +39,9 @@ public function transitionPermissions() {
'%to' => $transition->to()->label(),
]
),
+ 'dependencies' => [
+ $workflow->getConfigDependencyKey() => [$workflow->getConfigDependencyName()],
+ ],
];
}
}
diff --git a/core/modules/content_moderation/tests/src/Kernel/ContentModerationPermissionsTest.php b/core/modules/content_moderation/tests/src/Kernel/ContentModerationPermissionsTest.php
index 454b825baa..c1760feb23 100644
--- a/core/modules/content_moderation/tests/src/Kernel/ContentModerationPermissionsTest.php
+++ b/core/modules/content_moderation/tests/src/Kernel/ContentModerationPermissionsTest.php
@@ -110,14 +110,29 @@ public function permissionsTestCases() {
'use simple_workflow transition publish' => [
'title' => 'Simple Workflow workflow: Use Publish transition.',
'description' => 'Move content from Draft, Published states to Published state.',
+ 'dependencies' => [
+ 'config' => [
+ 'workflows.workflow.simple_workflow',
+ ],
+ ],
],
'use simple_workflow transition create_new_draft' => [
'title' => 'Simple Workflow workflow: Use Create New Draft transition.',
'description' => 'Move content from Draft, Published states to Draft state.',
+ 'dependencies' => [
+ 'config' => [
+ 'workflows.workflow.simple_workflow',
+ ],
+ ],
],
'use simple_workflow transition archive' => [
'title' => 'Simple Workflow workflow: Use Archive transition.',
'description' => 'Move content from Published state to Archived state.',
+ 'dependencies' => [
+ 'config' => [
+ 'workflows.workflow.simple_workflow',
+ ],
+ ],
],
],
],
diff --git a/core/modules/content_translation/src/ContentTranslationPermissions.php b/core/modules/content_translation/src/ContentTranslationPermissions.php
index ce1c71e9b8..032e8fcfd3 100644
--- a/core/modules/content_translation/src/ContentTranslationPermissions.php
+++ b/core/modules/content_translation/src/ContentTranslationPermissions.php
@@ -4,6 +4,7 @@
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -69,29 +70,25 @@ public static function create(ContainerInterface $container) {
* @return array
*/
public function contentPermissions() {
- $permission = [];
+ $permissions = [];
// Create a translate permission for each enabled entity type and (optionally)
// bundle.
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($permission_granularity = $entity_type->getPermissionGranularity()) {
- $t_args = ['@entity_label' => $entity_type->getSingularLabel()];
-
switch ($permission_granularity) {
case 'bundle':
foreach ($this->entityTypeBundleInfo->getBundleInfo($entity_type_id) as $bundle => $bundle_info) {
if ($this->contentTranslationManager->isEnabled($entity_type_id, $bundle)) {
- $t_args['%bundle_label'] = isset($bundle_info['label']) ? $bundle_info['label'] : $bundle;
- $permission["translate $bundle $entity_type_id"] = [
- 'title' => $this->t('Translate %bundle_label @entity_label', $t_args),
- ];
+ $permissions["translate $bundle $entity_type_id"] = $this->buildBundlePermission($entity_type, $bundle, $bundle_info);
}
}
break;
case 'entity_type':
if ($this->contentTranslationManager->isEnabled($entity_type_id)) {
- $permission["translate $entity_type_id"] = [
- 'title' => $this->t('Translate @entity_label', $t_args),
+ $permissions["translate $entity_type_id"] = [
+ 'title' => $this->t('Translate @entity_label', ['@entity_label' => $entity_type->getSingularLabel()]),
+ 'dependencies' => ['module' => [$entity_type->getProvider()]],
];
}
break;
@@ -99,6 +96,36 @@ public function contentPermissions() {
}
}
+ return $permissions;
+ }
+
+ /**
+ * Builds a content translation permission array for a bundle.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+ * The entity type.
+ * @param string $bundle
+ * The bundle to build the translation permission for.
+ * @param array $bundle_info
+ * The bundle info.
+ *
+ * @return array
+ * The permission details, keyed by 'title' and, if available,
+ * 'dependencies'.
+ */
+ private function buildBundlePermission(EntityTypeInterface $entity_type, string $bundle, array $bundle_info) {
+ $permission = [
+ 'title' => $this->t('Translate %bundle_label @entity_label', [
+ '@entity_label' => $entity_type->getSingularLabel(),
+ '%bundle_label' => $bundle_info['label'] ?? $bundle,
+ ]),
+ ];
+
+ // If the entity type uses bundle entities, add a dependency on the bundle.
+ $bundle_entity_type = $entity_type->getBundleEntityType();
+ if ($bundle_entity_type && $bundle_entity = $this->entityTypeManager->getStorage($bundle_entity_type)->load($bundle)) {
+ $permission['dependencies'][$bundle_entity->getConfigDependencyKey()][] = $bundle_entity->getConfigDependencyName();
+ }
return $permission;
}
diff --git a/core/modules/content_translation/tests/src/Kernel/ContentTranslationPermissionsTest.php b/core/modules/content_translation/tests/src/Kernel/ContentTranslationPermissionsTest.php
new file mode 100644
index 0000000000..772b30ebfc
--- /dev/null
+++ b/core/modules/content_translation/tests/src/Kernel/ContentTranslationPermissionsTest.php
@@ -0,0 +1,50 @@
+installEntitySchema('entity_test_mul');
+ $this->installEntitySchema('entity_test_mul_with_bundle');
+ EntityTestMulBundle::create([
+ 'id' => 'test',
+ 'label' => 'Test label',
+ 'description' => 'My test description',
+ ])->save();
+
+ }
+
+ /**
+ * Tests that enabling translation via the API triggers schema updates.
+ */
+ public function testPermissions() {
+ $this->container->get('content_translation.manager')->setEnabled('entity_test_mul', 'entity_test_mul', TRUE);
+ $this->container->get('content_translation.manager')->setEnabled('entity_test_mul_with_bundle', 'test', TRUE);
+ $permissions = \Drupal::service('user.permissions')->getPermissions();
+ $this->assertEquals(['entity_test'], $permissions['translate entity_test_mul']['dependencies']['module']);
+ $this->assertEquals(['entity_test.entity_test_mul_bundle.test'], $permissions['translate test entity_test_mul_with_bundle']['dependencies']['config']);
+ }
+
+}
diff --git a/core/modules/field/tests/src/Functional/Rest/FieldConfigResourceTestBase.php b/core/modules/field/tests/src/Functional/Rest/FieldConfigResourceTestBase.php
index def98a89e3..8f4bd3cb61 100644
--- a/core/modules/field/tests/src/Functional/Rest/FieldConfigResourceTestBase.php
+++ b/core/modules/field/tests/src/Functional/Rest/FieldConfigResourceTestBase.php
@@ -12,7 +12,7 @@ abstract class FieldConfigResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
- protected static $modules = ['field', 'node'];
+ protected static $modules = ['field', 'field_ui', 'node'];
/**
* {@inheritdoc}
diff --git a/core/modules/field/tests/src/Functional/Rest/FieldStorageConfigResourceTestBase.php b/core/modules/field/tests/src/Functional/Rest/FieldStorageConfigResourceTestBase.php
index 43f0273e9b..490c2678c5 100644
--- a/core/modules/field/tests/src/Functional/Rest/FieldStorageConfigResourceTestBase.php
+++ b/core/modules/field/tests/src/Functional/Rest/FieldStorageConfigResourceTestBase.php
@@ -10,7 +10,7 @@ abstract class FieldStorageConfigResourceTestBase extends EntityResourceTestBase
/**
* {@inheritdoc}
*/
- protected static $modules = ['node'];
+ protected static $modules = ['field_ui', 'node'];
/**
* {@inheritdoc}
@@ -88,13 +88,4 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
}
}
- /**
- * {@inheritdoc}
- */
- protected function getExpectedCacheContexts() {
- return [
- 'user.permissions',
- ];
- }
-
}
diff --git a/core/modules/field_ui/src/FieldUiPermissions.php b/core/modules/field_ui/src/FieldUiPermissions.php
index 3011250907..48fb956e59 100644
--- a/core/modules/field_ui/src/FieldUiPermissions.php
+++ b/core/modules/field_ui/src/FieldUiPermissions.php
@@ -48,17 +48,22 @@ public function fieldPermissions() {
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type->get('field_ui_base_route')) {
+ // The permissions depend on the module that provides the entity.
+ $dependencies = ['module' => [$entity_type->getProvider()]];
// Create a permission for each fieldable entity to manage
// the fields and the display.
$permissions['administer ' . $entity_type_id . ' fields'] = [
'title' => $this->t('%entity_label: Administer fields', ['%entity_label' => $entity_type->getLabel()]),
'restrict access' => TRUE,
+ 'dependencies' => $dependencies,
];
$permissions['administer ' . $entity_type_id . ' form display'] = [
'title' => $this->t('%entity_label: Administer form display', ['%entity_label' => $entity_type->getLabel()]),
+ 'dependencies' => $dependencies,
];
$permissions['administer ' . $entity_type_id . ' display'] = [
'title' => $this->t('%entity_label: Administer display', ['%entity_label' => $entity_type->getLabel()]),
+ 'dependencies' => $dependencies,
];
}
}
diff --git a/core/modules/filter/src/FilterPermissions.php b/core/modules/filter/src/FilterPermissions.php
index 4671c3aa6c..0ad98b48fe 100644
--- a/core/modules/filter/src/FilterPermissions.php
+++ b/core/modules/filter/src/FilterPermissions.php
@@ -59,6 +59,13 @@ public function permissions() {
'#markup' => $this->t('Warning: This permission may have security implications depending on how the text format is configured.'),
'#suffix' => '',
],
+ // This permission is generated on behalf of $format text format,
+ // therefore add this text format as a config dependency.
+ 'dependencies' => [
+ $format->getConfigDependencyKey() => [
+ $format->getConfigDependencyName(),
+ ],
+ ],
];
}
}
diff --git a/core/modules/filter/tests/src/Functional/FilterAdminTest.php b/core/modules/filter/tests/src/Functional/FilterAdminTest.php
index 8e682eb8e5..84a2f4daec 100644
--- a/core/modules/filter/tests/src/Functional/FilterAdminTest.php
+++ b/core/modules/filter/tests/src/Functional/FilterAdminTest.php
@@ -8,6 +8,7 @@
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\BrowserTestBase;
+use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
@@ -276,12 +277,19 @@ public function testFilterAdmin() {
$this->assertSession()->checkboxChecked('roles[' . RoleInterface::AUTHENTICATED_ID . ']');
$this->assertSession()->checkboxChecked('filters[' . $second_filter . '][status]');
$this->assertSession()->checkboxChecked('filters[' . $first_filter . '][status]');
+ /** @var \Drupal\user\Entity\Role $role */
+ \Drupal::entityTypeManager()->getStorage('user_role')->resetCache([RoleInterface::AUTHENTICATED_ID]);
+ $role = Role::load(RoleInterface::AUTHENTICATED_ID);
+ $this->assertTrue($role->hasPermission($format->getPermissionName()), 'The authenticated role has permission to use the filter.');
// Disable new filter.
$this->drupalGet('admin/config/content/formats/manage/' . $format->id() . '/disable');
$this->submitForm([], 'Disable');
$this->assertSession()->addressEquals('admin/config/content/formats');
$this->assertRaw(t('Disabled text format %format.', ['%format' => $edit['name']]));
+ \Drupal::entityTypeManager()->getStorage('user_role')->resetCache([RoleInterface::AUTHENTICATED_ID]);
+ $role = Role::load(RoleInterface::AUTHENTICATED_ID);
+ $this->assertFalse($role->hasPermission($format->getPermissionName()), 'The filter permission has been removed from the authenticated role');
// Allow authenticated users on full HTML.
$format = FilterFormat::load($full);
diff --git a/core/modules/filter/tests/src/Kernel/FilterCrudTest.php b/core/modules/filter/tests/src/Kernel/FilterCrudTest.php
index de59a00133..72df6bd4d2 100644
--- a/core/modules/filter/tests/src/Kernel/FilterCrudTest.php
+++ b/core/modules/filter/tests/src/Kernel/FilterCrudTest.php
@@ -100,6 +100,11 @@ public function verifyTextFormat($format) {
$this->assertEquals($format->get('weight'), $filter_format->get('weight'), new FormattableMarkup('filter_format_load: Proper weight for text format %format.', $t_args));
// Check that the filter was created in site default language.
$this->assertEquals($default_langcode, $format->language()->getId(), new FormattableMarkup('filter_format_load: Proper language code for text format %format.', $t_args));
+
+ // Verify the permission exists and has the correct dependencies.
+ $permissions = \Drupal::service('user.permissions')->getPermissions();
+ $this->assertTrue(isset($permissions[$format->getPermissionName()]));
+ $this->assertEquals(['config' => [$format->getConfigDependencyName()]], $permissions[$format->getPermissionName()]['dependencies']);
}
}
diff --git a/core/modules/jsonapi/tests/modules/jsonapi_test_field_access/jsonapi_test_field_access.permissions.yml b/core/modules/jsonapi/tests/modules/jsonapi_test_field_access/jsonapi_test_field_access.permissions.yml
new file mode 100644
index 0000000000..19875bb102
--- /dev/null
+++ b/core/modules/jsonapi/tests/modules/jsonapi_test_field_access/jsonapi_test_field_access.permissions.yml
@@ -0,0 +1,6 @@
+'field_jsonapi_test_entity_ref edit access':
+ title: 'Tests JSON:API field edit access'
+'field_jsonapi_test_entity_ref update access':
+ title: 'Tests JSON:API field update access'
+'field_jsonapi_test_entity_ref view access':
+ title: 'Tests JSON:API field view access'
diff --git a/core/modules/jsonapi/tests/modules/jsonapi_test_field_filter_access/jsonapi_test_field_filter_access.permissions.yml b/core/modules/jsonapi/tests/modules/jsonapi_test_field_filter_access/jsonapi_test_field_filter_access.permissions.yml
new file mode 100644
index 0000000000..ae9f235aad
--- /dev/null
+++ b/core/modules/jsonapi/tests/modules/jsonapi_test_field_filter_access/jsonapi_test_field_filter_access.permissions.yml
@@ -0,0 +1,2 @@
+'filter by spotlight field':
+ title: 'Tests JSON:API filter access'
diff --git a/core/modules/jsonapi/tests/src/Functional/ActionTest.php b/core/modules/jsonapi/tests/src/Functional/ActionTest.php
index 673b95df75..4fcc0e9e2b 100644
--- a/core/modules/jsonapi/tests/src/Functional/ActionTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/ActionTest.php
@@ -16,7 +16,7 @@ class ActionTest extends ResourceTestBase {
/**
* {@inheritdoc}
*/
- protected static $modules = ['user'];
+ protected static $modules = ['action'];
/**
* {@inheritdoc}
diff --git a/core/modules/jsonapi/tests/src/Functional/BaseFieldOverrideTest.php b/core/modules/jsonapi/tests/src/Functional/BaseFieldOverrideTest.php
index 1b5ab2c8ba..42aa3377e4 100644
--- a/core/modules/jsonapi/tests/src/Functional/BaseFieldOverrideTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/BaseFieldOverrideTest.php
@@ -16,7 +16,7 @@ class BaseFieldOverrideTest extends ResourceTestBase {
/**
* {@inheritdoc}
*/
- protected static $modules = ['field', 'node'];
+ protected static $modules = ['field', 'node', 'field_ui'];
/**
* {@inheritdoc}
diff --git a/core/modules/jsonapi/tests/src/Functional/EntityFormDisplayTest.php b/core/modules/jsonapi/tests/src/Functional/EntityFormDisplayTest.php
index 7ea9f58d4f..b0535b8a25 100644
--- a/core/modules/jsonapi/tests/src/Functional/EntityFormDisplayTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/EntityFormDisplayTest.php
@@ -16,7 +16,7 @@ class EntityFormDisplayTest extends ResourceTestBase {
/**
* {@inheritdoc}
*/
- protected static $modules = ['node'];
+ protected static $modules = ['node', 'field_ui'];
/**
* {@inheritdoc}
diff --git a/core/modules/jsonapi/tests/src/Functional/EntityViewDisplayTest.php b/core/modules/jsonapi/tests/src/Functional/EntityViewDisplayTest.php
index 163bc91165..53d376e171 100644
--- a/core/modules/jsonapi/tests/src/Functional/EntityViewDisplayTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/EntityViewDisplayTest.php
@@ -16,7 +16,7 @@ class EntityViewDisplayTest extends ResourceTestBase {
/**
* {@inheritdoc}
*/
- protected static $modules = ['node'];
+ protected static $modules = ['node', 'field_ui'];
/**
* {@inheritdoc}
diff --git a/core/modules/jsonapi/tests/src/Functional/FieldConfigTest.php b/core/modules/jsonapi/tests/src/Functional/FieldConfigTest.php
index a1cb1f002c..6e77a2fc1d 100644
--- a/core/modules/jsonapi/tests/src/Functional/FieldConfigTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/FieldConfigTest.php
@@ -19,7 +19,7 @@ class FieldConfigTest extends ResourceTestBase {
/**
* {@inheritdoc}
*/
- protected static $modules = ['field', 'node'];
+ protected static $modules = ['field', 'node', 'field_ui'];
/**
* {@inheritdoc}
diff --git a/core/modules/jsonapi/tests/src/Functional/FieldStorageConfigTest.php b/core/modules/jsonapi/tests/src/Functional/FieldStorageConfigTest.php
index 6fd3995225..0616821364 100644
--- a/core/modules/jsonapi/tests/src/Functional/FieldStorageConfigTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/FieldStorageConfigTest.php
@@ -15,7 +15,7 @@ class FieldStorageConfigTest extends ResourceTestBase {
/**
* {@inheritdoc}
*/
- protected static $modules = ['node'];
+ protected static $modules = ['node', 'field_ui'];
/**
* {@inheritdoc}
diff --git a/core/modules/jsonapi/tests/src/Functional/PathAliasTest.php b/core/modules/jsonapi/tests/src/Functional/PathAliasTest.php
index 6e28be6186..8fd7430a82 100644
--- a/core/modules/jsonapi/tests/src/Functional/PathAliasTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/PathAliasTest.php
@@ -16,7 +16,7 @@ class PathAliasTest extends ResourceTestBase {
/**
* {@inheritdoc}
*/
- protected static $modules = ['user'];
+ protected static $modules = ['path'];
/**
* {@inheritdoc}
diff --git a/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php b/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php
index b808211a06..2146b4b722 100644
--- a/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php
+++ b/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php
@@ -2372,7 +2372,7 @@ public function testPatchIndividual() {
$this->grantPermissionsToTestedRole([
'use editorial transition create_new_draft',
'use editorial transition archived_published',
- 'use editorial transition published',
+ 'use editorial transition publish',
]);
// Disallow PATCHing an entity that has a pending revision.
diff --git a/core/modules/jsonapi/tests/src/Functional/ViewTest.php b/core/modules/jsonapi/tests/src/Functional/ViewTest.php
index 57e23a8c64..1e0e8bf8ff 100644
--- a/core/modules/jsonapi/tests/src/Functional/ViewTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/ViewTest.php
@@ -15,7 +15,7 @@ class ViewTest extends ResourceTestBase {
/**
* {@inheritdoc}
*/
- protected static $modules = ['views'];
+ protected static $modules = ['views', 'views_ui'];
/**
* {@inheritdoc}
diff --git a/core/modules/layout_builder/src/LayoutBuilderOverridesPermissions.php b/core/modules/layout_builder/src/LayoutBuilderOverridesPermissions.php
index 20c6a5e22e..73d664455c 100644
--- a/core/modules/layout_builder/src/LayoutBuilderOverridesPermissions.php
+++ b/core/modules/layout_builder/src/LayoutBuilderOverridesPermissions.php
@@ -79,22 +79,33 @@ public function permissions() {
'@entity_type_plural' => $entity_type->getPluralLabel(),
'%bundle' => $this->bundleInfo->getBundleInfo($entity_type_id)[$bundle]['label'],
];
+ // These permissions are generated on behalf of $entity_display entity
+ // display, therefore add this entity display as a config dependency.
+ $dependencies = [
+ $entity_display->getConfigDependencyKey() => [
+ $entity_display->getConfigDependencyName(),
+ ],
+ ];
if ($entity_type->hasKey('bundle')) {
$permissions["configure all $bundle $entity_type_id layout overrides"] = [
'title' => $this->t('%entity_type - %bundle: Configure all layout overrides', $args),
'warning' => $this->t('Warning: Allows configuring the layout even if the user cannot edit the @entity_type_singular itself.', $args),
+ 'dependencies' => $dependencies,
];
$permissions["configure editable $bundle $entity_type_id layout overrides"] = [
'title' => $this->t('%entity_type - %bundle: Configure layout overrides for @entity_type_plural that the user can edit', $args),
+ 'dependencies' => $dependencies,
];
}
else {
$permissions["configure all $bundle $entity_type_id layout overrides"] = [
'title' => $this->t('%entity_type: Configure all layout overrides', $args),
'warning' => $this->t('Warning: Allows configuring the layout even if the user cannot edit the @entity_type_singular itself.', $args),
+ 'dependencies' => $dependencies,
];
$permissions["configure editable $bundle $entity_type_id layout overrides"] = [
'title' => $this->t('%entity_type: Configure layout overrides for @entity_type_plural that the user can edit', $args),
+ 'dependencies' => $dependencies,
];
}
}
diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderAccessTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderAccessTest.php
index d14af0ac93..2f92dbee69 100644
--- a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderAccessTest.php
+++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderAccessTest.php
@@ -66,8 +66,10 @@ protected function setUp(): void {
* Whether access is expected for a non-editable override.
* @param bool $editable_access
* Whether access is expected for an editable override.
+ * @param array $permission_dependencies
+ * An array of expected permission dependencies.
*/
- public function testAccessWithBundles(array $permissions, $default_access, $non_editable_access, $editable_access) {
+ public function testAccessWithBundles(array $permissions, $default_access, $non_editable_access, $editable_access, array $permission_dependencies) {
$permissions[] = 'edit own bundle_with_section_field content';
$permissions[] = 'access content';
$user = $this->drupalCreateUser($permissions);
@@ -126,6 +128,13 @@ public function testAccessWithBundles(array $permissions, $default_access, $non_
$this->drupalGet('node/' . $non_viewable_node->id() . '/layout');
$this->assertExpectedAccess(FALSE);
+
+ if (!empty($permission_dependencies)) {
+ $permission_definitions = \Drupal::service('user.permissions')->getPermissions();
+ foreach ($permission_dependencies as $permission => $expected_dependencies) {
+ $this->assertSame($expected_dependencies, $permission_definitions[$permission]['dependencies']);
+ }
+ }
}
/**
@@ -143,18 +152,29 @@ public function providerTestAccessWithBundles() {
TRUE,
TRUE,
TRUE,
+ [],
];
$data['override permissions'] = [
['configure all bundle_with_section_field node layout overrides'],
FALSE,
TRUE,
TRUE,
+ [
+ 'configure all bundle_with_section_field node layout overrides' => [
+ 'config' => ['core.entity_view_display.node.bundle_with_section_field.default'],
+ ],
+ ],
];
$data['editable override permissions'] = [
['configure editable bundle_with_section_field node layout overrides'],
FALSE,
FALSE,
TRUE,
+ [
+ 'configure editable bundle_with_section_field node layout overrides' => [
+ 'config' => ['core.entity_view_display.node.bundle_with_section_field.default'],
+ ],
+ ],
];
return $data;
}
@@ -164,7 +184,7 @@ public function providerTestAccessWithBundles() {
*
* @dataProvider providerTestAccessWithoutBundles
*/
- public function testAccessWithoutBundles(array $permissions, $default_access, $non_editable_access, $editable_access) {
+ public function testAccessWithoutBundles(array $permissions, $default_access, $non_editable_access, $editable_access, array $permission_dependencies) {
$permissions[] = 'access user profiles';
$user = $this->drupalCreateUser($permissions);
$this->drupalLogin($user);
@@ -202,6 +222,13 @@ public function testAccessWithoutBundles(array $permissions, $default_access, $n
$this->drupalGet('user/' . $non_viewable_user->id() . '/layout');
$this->assertExpectedAccess(FALSE);
+
+ if (!empty($permission_dependencies)) {
+ $permission_definitions = \Drupal::service('user.permissions')->getPermissions();
+ foreach ($permission_dependencies as $permission => $expected_dependencies) {
+ $this->assertSame($expected_dependencies, $permission_definitions[$permission]['dependencies']);
+ }
+ }
}
/**
@@ -219,18 +246,29 @@ public function providerTestAccessWithoutBundles() {
TRUE,
TRUE,
TRUE,
+ [],
];
$data['override permissions'] = [
['configure all user user layout overrides'],
FALSE,
TRUE,
TRUE,
+ [
+ 'configure all user user layout overrides' => [
+ 'config' => ['core.entity_view_display.user.user.default'],
+ ],
+ ],
];
$data['editable override permissions'] = [
['configure editable user user layout overrides'],
FALSE,
FALSE,
TRUE,
+ [
+ 'configure all user user layout overrides' => [
+ 'config' => ['core.entity_view_display.user.user.default'],
+ ],
+ ],
];
return $data;
}
diff --git a/core/modules/media/src/MediaPermissions.php b/core/modules/media/src/MediaPermissions.php
index 034d84ae0b..97526f82f8 100644
--- a/core/modules/media/src/MediaPermissions.php
+++ b/core/modules/media/src/MediaPermissions.php
@@ -52,7 +52,17 @@ public function mediaTypePermissions() {
$media_types = $this->entityTypeManager
->getStorage('media_type')->loadMultiple();
foreach ($media_types as $type) {
- $perms += $this->buildPermissions($type);
+ $perms += array_map(
+ function (array $perm) use ($type) {
+ // This permission is generated on behalf of a media type, therefore
+ // add the media type as a config dependency.
+ $perm['dependencies'] = [
+ $type->getConfigDependencyKey() => [$type->getConfigDependencyName()],
+ ];
+ return $perm;
+ },
+ $this->buildPermissions($type)
+ );
}
return $perms;
}
diff --git a/core/modules/media/tests/src/Kernel/MediaTest.php b/core/modules/media/tests/src/Kernel/MediaTest.php
index 9c7ca28eef..93cb160e39 100644
--- a/core/modules/media/tests/src/Kernel/MediaTest.php
+++ b/core/modules/media/tests/src/Kernel/MediaTest.php
@@ -34,4 +34,14 @@ public function testNameBaseField() {
$this->assertSame($field_definitions['name']->getDisplayOptions('view'), ['region' => 'hidden']);
}
+ /**
+ * Tests permissions based on a media type have the correct permissions.
+ */
+ public function testPermissions() {
+ $permissions = $this->container->get('user.permissions')->getPermissions();
+ $name = "create {$this->testMediaType->id()} media";
+ $this->assertArrayHasKey($name, $permissions);
+ $this->assertSame(['config' => [$this->testMediaType->getConfigDependencyName()]], $permissions[$name]['dependencies']);
+ }
+
}
diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/WidgetWithoutTypesTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/WidgetWithoutTypesTest.php
index 63beba8ad7..c43f22c7cd 100644
--- a/core/modules/media_library/tests/src/FunctionalJavascript/WidgetWithoutTypesTest.php
+++ b/core/modules/media_library/tests/src/FunctionalJavascript/WidgetWithoutTypesTest.php
@@ -129,7 +129,7 @@ public function testWidgetWithoutMediaTypes() {
// Visit a node create page.
$this->drupalGet('node/add/basic_page');
- $field_ui_uninstalled_message = 'There are no allowed media types configured for this field. Edit the field settings to select the allowed media types.';
+ $field_ui_uninstalled_message = 'There are no allowed media types configured for this field. Please contact the site administrator.';
// Assert the link is now longer part of the message.
$assert_session->elementNotExists('named', ['link', 'Edit the field settings']);
diff --git a/core/modules/node/src/NodePermissions.php b/core/modules/node/src/NodePermissions.php
index 30f9ee22c8..9c1c987ffb 100644
--- a/core/modules/node/src/NodePermissions.php
+++ b/core/modules/node/src/NodePermissions.php
@@ -21,9 +21,20 @@ class NodePermissions {
*/
public function nodeTypePermissions() {
$perms = [];
+ /** @var \Drupal\node\Entity\NodeType $type */
// Generate node permissions for all node types.
foreach (NodeType::loadMultiple() as $type) {
- $perms += $this->buildPermissions($type);
+ $perms += array_map(
+ function (array $perm) use ($type) {
+ // This permission is generated on behalf of a node type, therefore
+ // add the node type as a config dependency.
+ $perm['dependencies'] = [
+ $type->getConfigDependencyKey() => [$type->getConfigDependencyName()],
+ ];
+ return $perm;
+ },
+ $this->buildPermissions($type)
+ );
}
return $perms;
diff --git a/core/modules/path_alias/tests/src/Functional/Rest/PathAliasResourceTestBase.php b/core/modules/path_alias/tests/src/Functional/Rest/PathAliasResourceTestBase.php
index aaf5bf2e55..86ce7afe27 100644
--- a/core/modules/path_alias/tests/src/Functional/Rest/PathAliasResourceTestBase.php
+++ b/core/modules/path_alias/tests/src/Functional/Rest/PathAliasResourceTestBase.php
@@ -14,7 +14,7 @@ abstract class PathAliasResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
- protected static $modules = ['path_alias'];
+ protected static $modules = ['path', 'path_alias'];
/**
* {@inheritdoc}
@@ -116,11 +116,4 @@ protected function getNormalizedPostEntity() {
];
}
- /**
- * {@inheritdoc}
- */
- protected function getExpectedCacheContexts() {
- return ['user.permissions'];
- }
-
}
diff --git a/core/modules/rest/src/RestPermissions.php b/core/modules/rest/src/RestPermissions.php
index 7255d3f481..ae562cfc58 100644
--- a/core/modules/rest/src/RestPermissions.php
+++ b/core/modules/rest/src/RestPermissions.php
@@ -2,6 +2,7 @@
namespace Drupal\rest;
+use Drupal\Component\Utility\NestedArray;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\rest\Plugin\Type\ResourcePluginManager;
@@ -57,7 +58,15 @@ public function permissions() {
$resource_configs = $this->resourceConfigStorage->loadMultiple();
foreach ($resource_configs as $resource_config) {
$plugin = $resource_config->getResourcePlugin();
- $permissions = array_merge($permissions, $plugin->permissions());
+
+ // Add the rest resource configuration entity as a dependency to the
+ // permissions.
+ $permissions += array_map(function (array $permission_info) use ($resource_config) {
+ $merge_info['dependencies'][$resource_config->getConfigDependencyKey()] = [
+ $resource_config->getConfigDependencyName(),
+ ];
+ return NestedArray::mergeDeep($permission_info, $merge_info);
+ }, $plugin->permissions());
}
return $permissions;
}
diff --git a/core/modules/rest/tests/src/Kernel/Entity/RestPermissionsTest.php b/core/modules/rest/tests/src/Kernel/Entity/RestPermissionsTest.php
new file mode 100644
index 0000000000..f7de6943ff
--- /dev/null
+++ b/core/modules/rest/tests/src/Kernel/Entity/RestPermissionsTest.php
@@ -0,0 +1,49 @@
+ 'dblog',
+ 'plugin_id' => 'dblog',
+ 'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
+ 'configuration' => [
+ 'GET' => [
+ 'supported_auth' => ['cookie'],
+ 'supported_formats' => ['json'],
+ ],
+ ],
+ ])->save();
+
+ $permissions = $this->container->get('user.permissions')->getPermissions();
+ $this->assertArrayHasKey('restful get dblog', $permissions);
+ $this->assertSame(['config' => ['rest.resource.dblog']], $permissions['restful get dblog']['dependencies']);
+ }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml
index d34d94940d..31aa1d614c 100644
--- a/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml
+++ b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml
@@ -27,3 +27,7 @@ entity_test.entity_test_bundle.*:
description:
type: text
label: 'Description'
+
+entity_test.entity_test_mul_bundle.*:
+ type: entity_test.entity_test_bundle.*
+ label: 'Entity test mul bundle'
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module
index af58b40f13..6bfb766c79 100644
--- a/core/modules/system/tests/modules/entity_test/entity_test.module
+++ b/core/modules/system/tests/modules/entity_test/entity_test.module
@@ -72,6 +72,7 @@ function entity_test_entity_types($filter = NULL) {
$types[] = 'entity_test_base_field_display';
$types[] = 'entity_test_string_id';
$types[] = 'entity_test_no_id';
+ $types[] = 'entity_test_mul_with_bundle';
}
$types[] = 'entity_test_mulrev';
$types[] = 'entity_test_mulrev_changed';
@@ -225,7 +226,7 @@ function entity_test_entity_bundle_info() {
$bundles = [];
$entity_types = \Drupal::entityTypeManager()->getDefinitions();
foreach ($entity_types as $entity_type_id => $entity_type) {
- if ($entity_type->getProvider() == 'entity_test' && $entity_type_id != 'entity_test_with_bundle') {
+ if ($entity_type->getProvider() == 'entity_test' && !in_array($entity_type_id, ['entity_test_with_bundle', 'entity_test_mul_with_bundle'], TRUE)) {
$bundles[$entity_type_id] = \Drupal::state()->get($entity_type_id . '.bundles', [$entity_type_id => ['label' => 'Entity Test Bundle']]);
}
}
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.permissions.yml b/core/modules/system/tests/modules/entity_test/entity_test.permissions.yml
index 62e4d82b74..f2792ee2cf 100644
--- a/core/modules/system/tests/modules/entity_test/entity_test.permissions.yml
+++ b/core/modules/system/tests/modules/entity_test/entity_test.permissions.yml
@@ -18,6 +18,8 @@ view all entity_test_query_access entities:
title: 'view all entity_test_query_access entities'
edit own entity_test content:
title: 'Edit own entity_test content'
+create entity_test entity_test_with_bundle entities:
+ title: 'Create entity_test:entity_test_with_bundle content'
permission_callbacks:
- \Drupal\entity_test\EntityTestPermissions::entityTestBundlePermissions
diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulBundle.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulBundle.php
new file mode 100644
index 0000000000..65e58b719c
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulBundle.php
@@ -0,0 +1,77 @@
+description;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDescription($description) {
+ $this->description = $description;
+ return $this;
+ }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulWithBundle.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulWithBundle.php
new file mode 100644
index 0000000000..68a7e24bc6
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulWithBundle.php
@@ -0,0 +1,49 @@
+buildPermissions($vocabulary);
+ $permissions += array_map(
+ function (array $perm) use ($vocabulary) {
+ // This permission is generated on behalf of a vocabulary, therefore
+ // add the vocabulary as a config dependency.
+ $perm['dependencies'] = [
+ $vocabulary->getConfigDependencyKey() => [$vocabulary->getConfigDependencyName()],
+ ];
+ return $perm;
+ },
+ $this->buildPermissions($vocabulary)
+ );
}
return $permissions;
}
diff --git a/core/modules/taxonomy/tests/src/Functional/VocabularyPermissionsTest.php b/core/modules/taxonomy/tests/src/Functional/VocabularyPermissionsTest.php
index eaa802e313..2e4ff36ab8 100644
--- a/core/modules/taxonomy/tests/src/Functional/VocabularyPermissionsTest.php
+++ b/core/modules/taxonomy/tests/src/Functional/VocabularyPermissionsTest.php
@@ -220,6 +220,11 @@ public function testTaxonomyVocabularyOverviewPermissions() {
$assert_session->statusCodeEquals(200);
$assert_session->pageTextContains('No terms available');
$assert_session->linkExists('Add term');
+
+ // Ensure the dynamic vocabulary permissions have the correct dependencies.
+ $permissions = \Drupal::service('user.permissions')->getPermissions();
+ $this->assertTrue(isset($permissions['create terms in ' . $vocabulary1_id]));
+ $this->assertEquals(['config' => [$vocabulary1->getConfigDependencyName()]], $permissions['create terms in ' . $vocabulary1_id]['dependencies']);
}
/**
diff --git a/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTranslationTest.php b/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTranslationTest.php
index 3b95e9248d..83f019f5af 100644
--- a/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTranslationTest.php
+++ b/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTranslationTest.php
@@ -19,6 +19,7 @@ class MigrateTermNodeTranslationTest extends MigrateDrupal6TestBase {
'config_translation',
'content_translation',
'language',
+ 'locale',
'menu_ui',
'taxonomy',
];
diff --git a/core/modules/toolbar/tests/src/Functional/ToolbarAdminMenuTest.php b/core/modules/toolbar/tests/src/Functional/ToolbarAdminMenuTest.php
index 5daf95a7c1..74a1c410b1 100644
--- a/core/modules/toolbar/tests/src/Functional/ToolbarAdminMenuTest.php
+++ b/core/modules/toolbar/tests/src/Functional/ToolbarAdminMenuTest.php
@@ -7,6 +7,7 @@
use Drupal\Core\Url;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
+use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
@@ -114,6 +115,16 @@ protected function setUp(): void {
* implementations.
*/
public function testModuleStatusChangeSubtreesHashCacheClear() {
+ // Use an admin role to ensure the user has all available permissions. This
+ // results in the admin menu links changing as the taxonomy module is
+ // installed and uninstalled because the role will always have the
+ // 'administer taxonomy' permission if it exists.
+ $role = Role::load($this->createRole([]));
+ $role->setIsAdmin(TRUE);
+ $role->save();
+ $this->adminUser->addRole($role->id());
+ $this->adminUser->save();
+
// Uninstall a module.
$edit = [];
$edit['uninstall[taxonomy]'] = TRUE;
diff --git a/core/modules/user/migrations/d6_user_role.yml b/core/modules/user/migrations/d6_user_role.yml
index d6c2ca941f..03e30ade61 100644
--- a/core/modules/user/migrations/d6_user_role.yml
+++ b/core/modules/user/migrations/d6_user_role.yml
@@ -35,6 +35,11 @@ process:
- plugin: node_update_7008
- plugin: flatten
- plugin: filter_format_permission
+ # A special flag so we can migrate permissions that do not exist yet.
+ # @todo Remove in https://www.drupal.org/project/drupal/issues/2953111.
+ skip_missing_permission_deprecation:
+ plugin: default_value
+ default_value: true
destination:
plugin: entity:user_role
migration_dependencies:
diff --git a/core/modules/user/migrations/d7_user_role.yml b/core/modules/user/migrations/d7_user_role.yml
index 46885d7e75..4aaf8891a0 100644
--- a/core/modules/user/migrations/d7_user_role.yml
+++ b/core/modules/user/migrations/d7_user_role.yml
@@ -33,6 +33,11 @@ process:
'edit own forum topics': 'edit own forum content'
- plugin: flatten
weight: weight
+ # A special flag so we can migrate permissions that do not exist yet.
+ # @todo Remove in https://www.drupal.org/project/drupal/issues/2953111.
+ skip_missing_permission_deprecation:
+ plugin: default_value
+ default_value: true
destination:
plugin: entity:user_role
migration_dependencies:
diff --git a/core/modules/user/src/Entity/Role.php b/core/modules/user/src/Entity/Role.php
index 3512fee03e..5a73ecc6f8 100644
--- a/core/modules/user/src/Entity/Role.php
+++ b/core/modules/user/src/Entity/Role.php
@@ -3,6 +3,7 @@
namespace Drupal\user\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\user\RoleInterface;
@@ -193,4 +194,71 @@ public function preSave(EntityStorageInterface $storage) {
}
}
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ parent::calculateDependencies();
+ // Load all permission definitions.
+ $permission_definitions = \Drupal::service('user.permissions')->getPermissions();
+ $valid_permissions = array_intersect($this->permissions, array_keys($permission_definitions));
+ $invalid_permissions = array_diff($this->permissions, $valid_permissions);
+ if (!empty($invalid_permissions) && !$this->get('skip_missing_permission_deprecation')) {
+ @trigger_error('Adding non-existent permissions to a role is deprecated in drupal:9.3.0 and triggers a runtime exception before drupal:10.0.0. The incorrect permissions are "' . implode('", "', $invalid_permissions) . '". Permissions should be defined in a permissions.yml file or a permission callback. See https://www.drupal.org/node/3193348', E_USER_DEPRECATED);
+ }
+ foreach ($valid_permissions as $permission) {
+ // Depend on the module that is providing this permissions.
+ $this->addDependency('module', $permission_definitions[$permission]['provider']);
+ // Depend on any other dependencies defined by permissions granted to
+ // this role.
+ if (!empty($permission_definitions[$permission]['dependencies'])) {
+ $this->addDependencies($permission_definitions[$permission]['dependencies']);
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onDependencyRemoval(array $dependencies) {
+ $changed = parent::onDependencyRemoval($dependencies);
+ // Load all permission definitions.
+ $permission_definitions = \Drupal::service('user.permissions')->getPermissions();
+
+ // Convert config and content entity dependencies to a list of names to make
+ // it easier to check.
+ foreach (['content', 'config'] as $type) {
+ if (isset($dependencies[$type])) {
+ $dependencies[$type] = array_map(function (EntityInterface $entity) {
+ return $entity->getConfigDependencyName();
+ }, $dependencies[$type]);
+ }
+ }
+
+ // Remove any permissions from the role that are dependent on anything being
+ // deleted or uninstalled.
+ foreach ($this->permissions as $key => $permission) {
+ if (!isset($permission_definitions[$permission])) {
+ // If the permission is not defined then there's nothing we can do.
+ continue;
+ }
+ if (in_array($permission_definitions[$permission]['provider'], $dependencies['module'], TRUE)) {
+ unset($this->permissions[$key]);
+ $changed = TRUE;
+ }
+ elseif (isset($permission_definitions[$permission]['dependencies'])) {
+ foreach ($permission_definitions[$permission]['dependencies'] as $type => $list) {
+ if (array_intersect($list, $dependencies[$type])) {
+ unset($this->permissions[$key]);
+ $changed = TRUE;
+ continue 2;
+ }
+ }
+ }
+ }
+
+ return $changed;
+ }
+
}
diff --git a/core/modules/user/src/PermissionHandler.php b/core/modules/user/src/PermissionHandler.php
index dd04ab0118..dd568fee52 100644
--- a/core/modules/user/src/PermissionHandler.php
+++ b/core/modules/user/src/PermissionHandler.php
@@ -36,11 +36,15 @@
*
* # An array of callables used to generate dynamic permissions.
* permission_callbacks:
- * # Each item in the array should return an associative array with one or
- * # more permissions following the same keys as the permission defined above.
+ * # The callable should return an associative array with one or more
+ * # permissions. Each permission array can use the same keys as the example
+ * # permission defined above. Additionally, a dependencies key is supported.
+ * # For more information about permission dependencies see
+ * # PermissionHandlerInterface::getPermissions().
* - Drupal\filter\FilterPermissions::permissions
* @endcode
*
+ * @see \Drupal\user\PermissionHandlerInterface::getPermissions()
* @see filter.permissions.yml
* @see \Drupal\filter\FilterPermissions
* @see user_api
@@ -130,10 +134,10 @@ public function moduleProvidesPermissions($module_name) {
* Builds all permissions provided by .permissions.yml files.
*
* @return array[]
- * Each return permission is an array with the following keys:
- * - title: The title of the permission.
- * - description: The description of the permission, defaults to NULL.
- * - provider: The provider of the permission.
+ * An array with the same structure as
+ * PermissionHandlerInterface::getPermissions().
+ *
+ * @see \Drupal\user\PermissionHandlerInterface::getPermissions()
*/
protected function buildPermissionsYaml() {
$all_permissions = [];
@@ -193,10 +197,10 @@ protected function buildPermissionsYaml() {
* The permissions to be sorted.
*
* @return array[]
- * Each return permission is an array with the following keys:
- * - title: The title of the permission.
- * - description: The description of the permission, defaults to NULL.
- * - provider: The provider of the permission.
+ * An array with the same structure as
+ * PermissionHandlerInterface::getPermissions().
+ *
+ * @see \Drupal\user\PermissionHandlerInterface::getPermissions()
*/
protected function sortPermissions(array $all_permissions = []) {
// Get a list of all the modules providing permissions and sort by
diff --git a/core/modules/user/src/PermissionHandlerInterface.php b/core/modules/user/src/PermissionHandlerInterface.php
index 61526f339a..1a7011fedb 100644
--- a/core/modules/user/src/PermissionHandlerInterface.php
+++ b/core/modules/user/src/PermissionHandlerInterface.php
@@ -34,7 +34,21 @@ interface PermissionHandlerInterface {
* permissions to have a clear, consistent security warning that is the
* same across the site. Use the 'description' key instead to provide any
* information that is specific to the permission you are defining.
- * - provider: (optional) The provider name of the permission.
+ * - dependencies: (optional) An array of dependency entities used when
+ * building this permission, structured in the same way as the return
+ * of ConfigEntityInterface::calculateDependencies(). For example, if this
+ * permission was generated as effect of the existence of node type
+ * 'article', then value of the dependency key is:
+ * @code
+ * 'dependencies' => ['config' => ['node.type.article']]
+ * @endcode
+ * The module providing this permission doesn't have to be added as a
+ * dependency. It is automatically parsed, stored and retrieved from the
+ * 'provider' key.
+ * - provider: The provider name of the permission. This is set
+ * automatically to the module that provides the permission.yml file.
+ *
+ * @see \Drupal\Core\Config\Entity\ConfigEntityInterface::calculateDependencies()
*/
public function getPermissions();
diff --git a/core/modules/user/tests/modules/user_permissions_test/user_permissions_test.info.yml b/core/modules/user/tests/modules/user_permissions_test/user_permissions_test.info.yml
new file mode 100644
index 0000000000..fc4a8aa803
--- /dev/null
+++ b/core/modules/user/tests/modules/user_permissions_test/user_permissions_test.info.yml
@@ -0,0 +1,5 @@
+name: 'User permission tests'
+type: module
+description: 'Support module for user permission testing.'
+package: Testing
+version: VERSION
diff --git a/core/modules/user/tests/modules/user_permissions_test/user_permissions_test.permissions.yml b/core/modules/user/tests/modules/user_permissions_test/user_permissions_test.permissions.yml
new file mode 100644
index 0000000000..fa31800661
--- /dev/null
+++ b/core/modules/user/tests/modules/user_permissions_test/user_permissions_test.permissions.yml
@@ -0,0 +1,6 @@
+c:
+ title: 'Test permission'
+a:
+ title: 'Test permission'
+b:
+ title: 'Test permission'
diff --git a/core/modules/user/tests/src/Functional/Update/UserUpdateRoleDependenciesTest.php b/core/modules/user/tests/src/Functional/Update/UserUpdateRoleDependenciesTest.php
new file mode 100644
index 0000000000..c865a05b29
--- /dev/null
+++ b/core/modules/user/tests/src/Functional/Update/UserUpdateRoleDependenciesTest.php
@@ -0,0 +1,56 @@
+databaseDumpFiles = [
+ __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.8.0.bare.standard.php.gz',
+ ];
+ }
+
+ /**
+ * Tests that roles have dependencies and only existing permissions.
+ */
+ public function testRolePermissions() {
+ // Edit the role to have a non-existent permission.
+ $raw_config = $this->config('user.role.authenticated');
+ $permissions = $raw_config->get('permissions');
+ $permissions[] = 'does_not_exist';
+ $raw_config
+ ->set('permissions', $permissions)
+ ->save();
+
+ $authenticated = Role::load('authenticated');
+ $this->assertTrue($authenticated->hasPermission('does_not_exist'), 'Authenticated role has a permission that does not exist');
+ $this->assertEquals([], $authenticated->getDependencies());
+
+ $this->runUpdates();
+ $this->assertSession()->pageTextContains('The roles Anonymous user, Authenticated user have had non-existent permissions removed. Check the logs for details.');
+ $authenticated = Role::load('authenticated');
+ $this->assertFalse($authenticated->hasPermission('does_not_exist'), 'Authenticated role does not have a permission that does not exist');
+ $this->assertEquals(['config' => ['filter.format.basic_html'], 'module' => ['comment', 'contact', 'filter', 'shortcut', 'system']], $authenticated->getDependencies());
+
+ $this->drupalLogin($this->createUser(['access site reports']));
+ $this->drupalGet('admin/reports/dblog', ['query' => ['type[]' => 'update']]);
+ $this->clickLink('The role Authenticated user has had the following non-…');
+ $this->assertSession()->pageTextContains('The role Authenticated user has had the following non-existent permission(s) removed: use text format plain_text, does_not_exist.');
+ $this->getSession()->back();
+ $this->clickLink('The role Anonymous user has had the following non-…');
+ $this->assertSession()->pageTextContains('The role Anonymous user has had the following non-existent permission(s) removed: use text format plain_text.');
+ }
+
+}
diff --git a/core/modules/user/tests/src/Kernel/UserRoleDeleteTest.php b/core/modules/user/tests/src/Kernel/UserRoleDeleteTest.php
index 01c387dee2..bbfa5bfe22 100644
--- a/core/modules/user/tests/src/Kernel/UserRoleDeleteTest.php
+++ b/core/modules/user/tests/src/Kernel/UserRoleDeleteTest.php
@@ -2,7 +2,10 @@
namespace Drupal\Tests\user\Kernel;
+use Drupal\filter\Entity\FilterFormat;
use Drupal\KernelTests\KernelTestBase;
+use Drupal\node\Entity\NodeType;
+use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
/**
@@ -71,4 +74,83 @@ public function testRoleDeleteUserRoleReferenceDelete() {
}
+ /**
+ * Tests the removal of user role dependencies.
+ */
+ public function testDependenciesRemoval() {
+ $this->enableModules(['node', 'filter']);
+ /** @var \Drupal\user\RoleStorage $role_storage */
+ $role_storage = $this->container->get('entity_type.manager')->getStorage('user_role');
+
+ /** @var \Drupal\user\RoleInterface $role */
+ $role = Role::create([
+ 'id' => 'test_role',
+ 'label' => $this->randomString(),
+ ]);
+ $role->save();
+
+ /** @var \Drupal\node\NodeTypeInterface $node_type */
+ $node_type = NodeType::create([
+ 'type' => mb_strtolower($this->randomMachineName()),
+ 'name' => $this->randomString(),
+ ]);
+ $node_type->save();
+ // Create a new text format to be used by role $role.
+ $format = FilterFormat::create([
+ 'format' => mb_strtolower($this->randomMachineName()),
+ 'name' => $this->randomString(),
+ ]);
+ $format->save();
+
+ $permission_format = "use text format {$format->id()}";
+ // Add two permissions with the same dependency to ensure both are removed
+ // and the role is not deleted.
+ $permission_node_type = "edit any {$node_type->id()} content";
+ $permission_node_type_create = "create {$node_type->id()} content";
+
+ // Grant $role permission to access content, use $format, edit $node_type.
+ $role
+ ->grantPermission('access content')
+ ->grantPermission($permission_format)
+ ->grantPermission($permission_node_type)
+ ->grantPermission($permission_node_type_create)
+ ->save();
+
+ // The role $role has the permissions to use $format and edit $node_type.
+ $role_storage->resetCache();
+ $role = Role::load($role->id());
+ $this->assertTrue($role->hasPermission($permission_format));
+ $this->assertTrue($role->hasPermission($permission_node_type));
+ $this->assertTrue($role->hasPermission($permission_node_type_create));
+
+ // Remove the format.
+ $format->delete();
+
+ // The $role config entity exists after removing the config dependency.
+ $role_storage->resetCache();
+ $role = Role::load($role->id());
+ $this->assertNotNull($role);
+ // The $format permission should have been revoked.
+ $this->assertFalse($role->hasPermission($permission_format));
+ $this->assertTrue($role->hasPermission($permission_node_type));
+ $this->assertTrue($role->hasPermission($permission_node_type_create));
+
+ // We have to manually trigger the removal of configuration belonging to the
+ // module because KernelTestBase::disableModules() is not aware of this.
+ $this->container->get('config.manager')->uninstall('module', 'node');
+ // Disable the node module.
+ $this->disableModules(['node']);
+
+ // The $role config entity exists after removing the module dependency.
+ $role_storage->resetCache();
+ $role = Role::load($role->id());
+ $this->assertNotNull($role);
+ // The $node_type permission should have been revoked too.
+ $this->assertFalse($role->hasPermission($permission_format));
+ $this->assertFalse($role->hasPermission($permission_node_type));
+ $this->assertFalse($role->hasPermission($permission_node_type_create));
+ // The 'access content' permission should not have been revoked.
+ $this->assertTrue($role->hasPermission('access content'));
+ }
+
}
diff --git a/core/modules/user/tests/src/Kernel/UserRoleEntityTest.php b/core/modules/user/tests/src/Kernel/UserRoleEntityTest.php
index b597c3ddae..842afd17c2 100644
--- a/core/modules/user/tests/src/Kernel/UserRoleEntityTest.php
+++ b/core/modules/user/tests/src/Kernel/UserRoleEntityTest.php
@@ -10,7 +10,7 @@
*/
class UserRoleEntityTest extends KernelTestBase {
- protected static $modules = ['system', 'user'];
+ protected static $modules = ['system', 'user', 'user_permissions_test'];
public function testOrderOfPermissions() {
$role = Role::create(['id' => 'test_role']);
@@ -27,4 +27,25 @@ public function testOrderOfPermissions() {
$this->assertEquals($role->getPermissions(), ['a', 'b', 'c']);
}
+ /**
+ * @group legacy
+ */
+ public function testGrantingNonExistentPermission() {
+ $role = Role::create(['id' => 'test_role']);
+
+ // A single permission that does not exist.
+ $this->expectDeprecation('Adding non-existent permissions to a role is deprecated in drupal:9.3.0 and triggers a runtime exception before drupal:10.0.0. The incorrect permissions are "does not exist". Permissions should be defined in a permissions.yml file or a permission callback. See https://www.drupal.org/node/3193348');
+ $role->grantPermission('does not exist')
+ ->save();
+
+ // A multiple permissions that do not exist.
+ $this->expectDeprecation('Adding non-existent permissions to a role is deprecated in drupal:9.3.0 and triggers a runtime exception before drupal:10.0.0. The incorrect permissions are "does not exist", "also does not exist". Permissions should be defined in a permissions.yml file or a permission callback. See https://www.drupal.org/node/3193348');
+ $role->grantPermission('does not exist')
+ ->grantPermission('also does not exist')
+ ->save();
+
+ // Ensure that calling ::onDependencyRemoval does not produce PHP notices.
+ $this->assertFalse($role->onDependencyRemoval(['module' => []]));
+ }
+
}
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index ef7bd083d1..a3789743cd 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -20,6 +20,7 @@
use Drupal\Core\Site\Settings;
use Drupal\Core\Url;
use Drupal\image\Plugin\Field\FieldType\ImageItem;
+use Drupal\filter\FilterFormatInterface;
use Drupal\system\Entity\Action;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
@@ -1305,3 +1306,16 @@ function user_form_system_regional_settings_submit($form, FormStateInterface $fo
->set('timezone.user.default', $form_state->getValue('user_default_timezone'))
->save();
}
+
+/**
+ * Implements hook_filter_format_disable().
+ */
+function user_filter_format_disable(FilterFormatInterface $filter_format) {
+ // Remove permissions from any roles.
+ /** @var \Drupal\user\Entity\Role $role */
+ foreach (Role::loadMultiple() as $role) {
+ if ($role->onDependencyRemoval(['config' => [$filter_format], 'module' => []])) {
+ $role->save();
+ }
+ }
+}
diff --git a/core/modules/user/user.post_update.php b/core/modules/user/user.post_update.php
index 154cd03590..14662df8ef 100644
--- a/core/modules/user/user.post_update.php
+++ b/core/modules/user/user.post_update.php
@@ -5,6 +5,10 @@
* Post update functions for User module.
*/
+use Drupal\Core\Config\Entity\ConfigEntityUpdater;
+use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
+use Drupal\user\Entity\Role;
+
/**
* Implements hook_removed_post_updates().
*/
@@ -13,3 +17,33 @@ function user_removed_post_updates() {
'user_post_update_enforce_order_of_permissions' => '9.0.0',
];
}
+
+/**
+ * Calculate role dependencies and remove non-existent permissions.
+ */
+function user_post_update_update_roles(&$sandbox = NULL) {
+ $cleaned_roles = [];
+ $existing_permissions = array_keys(\Drupal::service('user.permissions')->getPermissions());
+ \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'user_role', function (Role $role) use ($existing_permissions, &$cleaned_roles) {
+ $removed_permissions = array_diff($role->getPermissions(), $existing_permissions);
+ if (!empty($removed_permissions)) {
+ $cleaned_roles[] = $role->label();
+ \Drupal::logger('update')->notice(
+ 'The role %role has had the following non-existent permission(s) removed: %permissions.',
+ ['%role' => $role->label(), '%permissions' => implode(', ', $removed_permissions)]
+ );
+ }
+ $permissions = array_intersect($role->getPermissions(), $existing_permissions);
+ $role->set('permissions', $permissions);
+ return TRUE;
+ });
+
+ if (!empty($cleaned_roles)) {
+ return new PluralTranslatableMarkup(
+ count($cleaned_roles),
+ 'The role %role_list has had non-existent permissions removed. Check the logs for details.',
+ 'The roles %role_list have had non-existent permissions removed. Check the logs for details.',
+ ['%role_list' => implode(', ', $cleaned_roles)]
+ );
+ }
+}
diff --git a/core/modules/views/tests/src/Functional/Rest/ViewResourceTestBase.php b/core/modules/views/tests/src/Functional/Rest/ViewResourceTestBase.php
index cd6a27e594..321510b054 100644
--- a/core/modules/views/tests/src/Functional/Rest/ViewResourceTestBase.php
+++ b/core/modules/views/tests/src/Functional/Rest/ViewResourceTestBase.php
@@ -86,14 +86,4 @@ protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
}
- /**
- * {@inheritdoc}
- */
- protected function getExpectedCacheContexts() {
- return [
- 'url.site',
- 'user.permissions',
- ];
- }
-
}
diff --git a/core/profiles/demo_umami/config/install/user.role.anonymous.yml b/core/profiles/demo_umami/config/install/user.role.anonymous.yml
index b860296d75..820b8454fb 100644
--- a/core/profiles/demo_umami/config/install/user.role.anonymous.yml
+++ b/core/profiles/demo_umami/config/install/user.role.anonymous.yml
@@ -1,6 +1,14 @@
langcode: en
status: true
-dependencies: { }
+dependencies:
+ config:
+ - filter.format.restricted_html
+ module:
+ - contact
+ - filter
+ - media
+ - search
+ - system
id: anonymous
label: 'Anonymous user'
weight: 0
diff --git a/core/profiles/demo_umami/config/install/user.role.authenticated.yml b/core/profiles/demo_umami/config/install/user.role.authenticated.yml
index ac4e409555..8334961e2c 100644
--- a/core/profiles/demo_umami/config/install/user.role.authenticated.yml
+++ b/core/profiles/demo_umami/config/install/user.role.authenticated.yml
@@ -1,6 +1,15 @@
langcode: en
status: true
-dependencies: { }
+dependencies:
+ config:
+ - filter.format.basic_html
+ module:
+ - contact
+ - filter
+ - media
+ - search
+ - shortcut
+ - system
id: authenticated
label: 'Authenticated user'
weight: 1
diff --git a/core/profiles/demo_umami/config/install/user.role.author.yml b/core/profiles/demo_umami/config/install/user.role.author.yml
index e0665dc1ee..3e30f9e01d 100644
--- a/core/profiles/demo_umami/config/install/user.role.author.yml
+++ b/core/profiles/demo_umami/config/install/user.role.author.yml
@@ -1,6 +1,23 @@
langcode: en
status: true
-dependencies: { }
+dependencies:
+ config:
+ - node.type.article
+ - node.type.page
+ - node.type.recipe
+ - taxonomy.vocabulary.recipe_category
+ - taxonomy.vocabulary.tags
+ - workflows.workflow.editorial
+ module:
+ - content_moderation
+ - contextual
+ - file
+ - node
+ - path
+ - quickedit
+ - system
+ - taxonomy
+ - toolbar
id: author
label: Author
weight: 3
diff --git a/core/profiles/demo_umami/config/install/user.role.editor.yml b/core/profiles/demo_umami/config/install/user.role.editor.yml
index 1149947f1a..83c4a2f221 100644
--- a/core/profiles/demo_umami/config/install/user.role.editor.yml
+++ b/core/profiles/demo_umami/config/install/user.role.editor.yml
@@ -1,6 +1,25 @@
langcode: en
status: true
-dependencies: { }
+dependencies:
+ config:
+ - node.type.article
+ - node.type.page
+ - node.type.recipe
+ - taxonomy.vocabulary.recipe_category
+ - taxonomy.vocabulary.tags
+ - workflows.workflow.editorial
+ module:
+ - content_moderation
+ - content_translation
+ - contextual
+ - file
+ - node
+ - path
+ - quickedit
+ - shortcut
+ - system
+ - taxonomy
+ - toolbar
id: editor
label: Editor
weight: 4
diff --git a/core/profiles/standard/config/install/user.role.anonymous.yml b/core/profiles/standard/config/install/user.role.anonymous.yml
index 6833f166ec..5674329ecf 100644
--- a/core/profiles/standard/config/install/user.role.anonymous.yml
+++ b/core/profiles/standard/config/install/user.role.anonymous.yml
@@ -1,6 +1,14 @@
langcode: en
status: true
-dependencies: { }
+dependencies:
+ config:
+ - filter.format.restricted_html
+ module:
+ - comment
+ - contact
+ - filter
+ - search
+ - system
id: anonymous
label: 'Anonymous user'
weight: 0
diff --git a/core/profiles/standard/config/install/user.role.authenticated.yml b/core/profiles/standard/config/install/user.role.authenticated.yml
index b5487dbc46..2442711ddc 100644
--- a/core/profiles/standard/config/install/user.role.authenticated.yml
+++ b/core/profiles/standard/config/install/user.role.authenticated.yml
@@ -1,6 +1,15 @@
langcode: en
status: true
-dependencies: { }
+dependencies:
+ config:
+ - filter.format.basic_html
+ module:
+ - comment
+ - contact
+ - filter
+ - search
+ - shortcut
+ - system
id: authenticated
label: 'Authenticated user'
weight: 1
diff --git a/core/tests/Drupal/FunctionalTests/Rest/BaseFieldOverrideResourceTestBase.php b/core/tests/Drupal/FunctionalTests/Rest/BaseFieldOverrideResourceTestBase.php
index 4a361bfb73..35f660eead 100644
--- a/core/tests/Drupal/FunctionalTests/Rest/BaseFieldOverrideResourceTestBase.php
+++ b/core/tests/Drupal/FunctionalTests/Rest/BaseFieldOverrideResourceTestBase.php
@@ -11,7 +11,7 @@ abstract class BaseFieldOverrideResourceTestBase extends EntityResourceTestBase
/**
* {@inheritdoc}
*/
- protected static $modules = ['field', 'node'];
+ protected static $modules = ['field', 'field_ui', 'node'];
/**
* {@inheritdoc}
diff --git a/core/tests/Drupal/FunctionalTests/Rest/EntityFormDisplayResourceTestBase.php b/core/tests/Drupal/FunctionalTests/Rest/EntityFormDisplayResourceTestBase.php
index 5ccd9ad5d1..a2d09cff12 100644
--- a/core/tests/Drupal/FunctionalTests/Rest/EntityFormDisplayResourceTestBase.php
+++ b/core/tests/Drupal/FunctionalTests/Rest/EntityFormDisplayResourceTestBase.php
@@ -11,7 +11,7 @@ abstract class EntityFormDisplayResourceTestBase extends EntityResourceTestBase
/**
* {@inheritdoc}
*/
- protected static $modules = ['node'];
+ protected static $modules = ['node', 'field_ui'];
/**
* {@inheritdoc}
diff --git a/core/tests/Drupal/FunctionalTests/Rest/EntityViewDisplayResourceTestBase.php b/core/tests/Drupal/FunctionalTests/Rest/EntityViewDisplayResourceTestBase.php
index 50f6f3b6f3..6d50d781bf 100644
--- a/core/tests/Drupal/FunctionalTests/Rest/EntityViewDisplayResourceTestBase.php
+++ b/core/tests/Drupal/FunctionalTests/Rest/EntityViewDisplayResourceTestBase.php
@@ -11,7 +11,7 @@ abstract class EntityViewDisplayResourceTestBase extends EntityResourceTestBase
/**
* {@inheritdoc}
*/
- protected static $modules = ['node'];
+ protected static $modules = ['node', 'field_ui'];
/**
* {@inheritdoc}