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}