From cffb02aad63664e7930a3d2ec798a8feb054af82 Mon Sep 17 00:00:00 2001
From: Lee Rowlands <lee.rowlands@previousnext.com.au>
Date: Fri, 9 Jul 2021 14:50:54 +1000
Subject: [PATCH] Issue #2571235 by alexpott, claudiu.cristea, dawehner, Wim
 Leers, gabesullice, IJsbrandy, benjifisher, alisonjo315, larowlan, Berdir,
 effulgentsia, longwave, herom, dmouse, heddn, catch: [regression] Roles
 should depend on objects that are building the granted permissions

---
 .../Entity/BundlePermissionHandlerTrait.php   | 42 ++++++++++
 .../tests/src/Functional/BasicAuthTest.php    |  2 +-
 .../d6/MigrateBlockContentTranslationTest.php |  1 +
 .../d7/MigrateBlockContentTranslationTest.php |  1 +
 .../ConfigInstallProfileOverrideTest.php      |  1 +
 .../content_moderation/src/Permissions.php    |  3 +
 .../ContentModerationPermissionsTest.php      | 15 ++++
 .../src/ContentTranslationPermissions.php     | 47 +++++++++--
 .../ContentTranslationPermissionsTest.php     | 57 +++++++++++++
 .../Rest/FieldConfigResourceTestBase.php      |  2 +-
 .../FieldStorageConfigResourceTestBase.php    | 11 +--
 .../field_ui/src/FieldUiPermissions.php       |  5 ++
 core/modules/filter/src/FilterPermissions.php |  7 ++
 .../tests/src/Functional/FilterAdminTest.php  |  8 ++
 .../tests/src/Kernel/FilterCrudTest.php       |  5 ++
 .../jsonapi_test_field_access.permissions.yml |  6 ++
 ...i_test_field_filter_access.permissions.yml |  2 +
 .../tests/src/Functional/ActionTest.php       |  2 +-
 .../src/Functional/BaseFieldOverrideTest.php  |  2 +-
 .../src/Functional/EntityFormDisplayTest.php  |  2 +-
 .../src/Functional/EntityViewDisplayTest.php  |  2 +-
 .../tests/src/Functional/FieldConfigTest.php  |  2 +-
 .../src/Functional/FieldStorageConfigTest.php |  2 +-
 .../tests/src/Functional/PathAliasTest.php    |  2 +-
 .../tests/src/Functional/ResourceTestBase.php |  2 +-
 .../jsonapi/tests/src/Functional/ViewTest.php |  2 +-
 .../src/LayoutBuilderOverridesPermissions.php | 11 +++
 .../Functional/LayoutBuilderAccessTest.php    | 42 +++++++++-
 core/modules/media/src/MediaPermissions.php   | 12 +--
 .../media/tests/src/Kernel/MediaTest.php      | 10 +++
 .../WidgetWithoutTypesTest.php                |  2 +-
 core/modules/node/src/NodePermissions.php     | 11 +--
 .../Rest/PathAliasResourceTestBase.php        |  9 +-
 core/modules/rest/src/RestPermissions.php     | 11 ++-
 .../src/Kernel/Entity/RestPermissionsTest.php | 49 +++++++++++
 .../config/schema/entity_test.schema.yml      |  4 +
 .../modules/entity_test/entity_test.module    |  6 +-
 .../entity_test/entity_test.permissions.yml   |  2 +
 .../src/Entity/EntityTestMulBundle.php        | 77 +++++++++++++++++
 .../src/Entity/EntityTestMulWithBundle.php    | 49 +++++++++++
 .../system_test/system_test.permissions.yml   |  3 +
 .../Rest/ActionResourceTestBase.php           | 11 +--
 .../taxonomy/src/TaxonomyPermissions.php      |  9 +-
 .../Functional/VocabularyPermissionsTest.php  |  5 ++
 .../d6/MigrateTermNodeTranslationTest.php     |  1 +
 .../src/Functional/ToolbarAdminMenuTest.php   | 11 +++
 core/modules/user/migrations/d6_user_role.yml |  5 ++
 core/modules/user/migrations/d7_user_role.yml |  5 ++
 core/modules/user/src/Entity/Role.php         | 68 +++++++++++++++
 core/modules/user/src/PermissionHandler.php   | 24 +++---
 .../user/src/PermissionHandlerInterface.php   | 16 +++-
 .../user_permissions_test.info.yml            |  5 ++
 .../user_permissions_test.permissions.yml     |  6 ++
 .../Update/UserUpdateRoleDependenciesTest.php | 56 +++++++++++++
 .../tests/src/Kernel/UserRoleDeleteTest.php   | 82 +++++++++++++++++++
 .../tests/src/Kernel/UserRoleEntityTest.php   | 21 ++++-
 core/modules/user/user.module                 | 15 ++++
 core/modules/user/user.post_update.php        | 34 ++++++++
 .../Functional/Rest/ViewResourceTestBase.php  | 10 ---
 .../config/install/user.role.anonymous.yml    | 10 ++-
 .../install/user.role.authenticated.yml       | 11 ++-
 .../config/install/user.role.author.yml       | 19 ++++-
 .../config/install/user.role.editor.yml       | 21 ++++-
 .../config/install/user.role.anonymous.yml    | 10 ++-
 .../install/user.role.authenticated.yml       | 11 ++-
 .../install/user.role.content_editor.yml      | 17 +++-
 .../BaseFieldOverrideResourceTestBase.php     |  2 +-
 .../EntityFormDisplayResourceTestBase.php     |  2 +-
 .../EntityViewDisplayResourceTestBase.php     |  2 +-
 .../BundlePermissionHandlerTraitTest.php      | 81 ++++++++++++++++++
 70 files changed, 994 insertions(+), 107 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Entity/BundlePermissionHandlerTrait.php
 create mode 100644 core/modules/content_translation/tests/src/Kernel/ContentTranslationPermissionsTest.php
 create mode 100644 core/modules/jsonapi/tests/modules/jsonapi_test_field_access/jsonapi_test_field_access.permissions.yml
 create mode 100644 core/modules/jsonapi/tests/modules/jsonapi_test_field_filter_access/jsonapi_test_field_filter_access.permissions.yml
 create mode 100644 core/modules/rest/tests/src/Kernel/Entity/RestPermissionsTest.php
 create mode 100644 core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulBundle.php
 create mode 100644 core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulWithBundle.php
 create mode 100644 core/modules/user/tests/modules/user_permissions_test/user_permissions_test.info.yml
 create mode 100644 core/modules/user/tests/modules/user_permissions_test/user_permissions_test.permissions.yml
 create mode 100644 core/modules/user/tests/src/Functional/Update/UserUpdateRoleDependenciesTest.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/Entity/BundlePermissionHandlerTraitTest.php

diff --git a/core/lib/Drupal/Core/Entity/BundlePermissionHandlerTrait.php b/core/lib/Drupal/Core/Entity/BundlePermissionHandlerTrait.php
new file mode 100644
index 000000000000..99f6daf5baba
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/BundlePermissionHandlerTrait.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Drupal\Core\Entity;
+
+/**
+ * Provides a method to simplify generating bundle level permissions.
+ */
+trait BundlePermissionHandlerTrait {
+
+  /**
+   * Builds a permissions array for the supplied bundles.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface[] $bundles
+   *   An array of bundles to generate permissions for.
+   * @param callable $permission_builder
+   *   A callable to generate the permissions for a particular bundle. Returns
+   *   an array of permissions. See PermissionHandlerInterface::getPermissions()
+   *   for the array structure.
+   *
+   * @return array
+   *   Permissions array. See PermissionHandlerInterface::getPermissions() for
+   *   the array structure.
+   *
+   * @see \Drupal\user\PermissionHandlerInterface::getPermissions()
+   */
+  protected function generatePermissions(array $bundles, callable $permission_builder) {
+    $permissions = [];
+    foreach ($bundles as $bundle) {
+      $permissions += array_map(
+        function (array $perm) use ($bundle) {
+          // This permission is generated on behalf of a bundle, therefore
+          // add the bundle as a config dependency.
+          $perm['dependencies'][$bundle->getConfigDependencyKey()][] = $bundle->getConfigDependencyName();
+          return $perm;
+        },
+        $permission_builder($bundle)
+      );
+    }
+    return $permissions;
+  }
+
+}
diff --git a/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php b/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php
index 7fe4b98d0349..7a2f13fb3a8d 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 4005419b9005..f9673cde760e 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 d008562f6bd6..9f3a5c77a028 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 e8b9afa1ce05..4b723d3b4e00 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 af0e104a7cdd..68639faf306e 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 454b825baab2..c1760feb2372 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' => '<em class="placeholder">Simple Workflow</em> workflow: Use <em class="placeholder">Publish</em> transition.',
             'description' => 'Move content from <em class="placeholder">Draft, Published</em> states to <em class="placeholder">Published</em> state.',
+            'dependencies' => [
+              'config' => [
+                'workflows.workflow.simple_workflow',
+              ],
+            ],
           ],
           'use simple_workflow transition create_new_draft' => [
             'title' => '<em class="placeholder">Simple Workflow</em> workflow: Use <em class="placeholder">Create New Draft</em> transition.',
             'description' => 'Move content from <em class="placeholder">Draft, Published</em> states to <em class="placeholder">Draft</em> state.',
+            'dependencies' => [
+              'config' => [
+                'workflows.workflow.simple_workflow',
+              ],
+            ],
           ],
           'use simple_workflow transition archive' => [
             'title' => '<em class="placeholder">Simple Workflow</em> workflow: Use <em class="placeholder">Archive</em> transition.',
             'description' => 'Move content from <em class="placeholder">Published</em> state to <em class="placeholder">Archived</em> 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 ce1c71e9b8fe..bb051a308f23 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,38 @@ 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 '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();
+    }
+    else {
+      $permission['dependencies']['module'][] = $entity_type->getProvider();
+    }
     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 000000000000..170ea5f893f8
--- /dev/null
+++ b/core/modules/content_translation/tests/src/Kernel/ContentTranslationPermissionsTest.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Drupal\Tests\content_translation\Kernel;
+
+use Drupal\entity_test\Entity\EntityTestMulBundle;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests the content translation dynamic permissions.
+ *
+ * @group content_translation
+ *
+ * @coversDefaultClass \Drupal\content_translation\ContentTranslationPermissions
+ */
+class ContentTranslationPermissionsTest extends KernelTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  protected static $modules = ['system', 'language', 'content_translation', 'user', 'entity_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    $this->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 = $this->container->get('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']);
+
+    // Ensure bundle permission granularity works for bundles not based on
+    // configuration.
+    $this->container->get('state')->set('entity_test_mul.permission_granularity', 'bundle');
+    $this->container->get('entity_type.manager')->clearCachedDefinitions();
+    $permissions = $this->container->get('user.permissions')->getPermissions();
+    $this->assertEquals(['entity_test'], $permissions['translate entity_test_mul 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 def98a89e318..8f4bd3cb61d5 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 43f0273e9b99..490c2678c51d 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 301125090739..48fb956e5963 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 4671c3aa6c32..0ad98b48fef4 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' => '</em>',
           ],
+          // 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 8e682eb8e56b..84a2f4daec42 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 de59a0013308..72df6bd4d251 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 000000000000..19875bb102f5
--- /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 000000000000..ae9f235aad99
--- /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 673b95df75a1..4fcc0e9e2bb7 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 1b5ab2c8ba25..42aa3377e442 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 7ea9f58d4ff3..b0535b8a25af 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 163bc91165b3..53d376e171cc 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 a1cb1f002cf3..6e77a2fc1d39 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 6fd39952256f..061682136479 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 6e28be618625..8fd7430a8230 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 e0d9048f42e5..626a4c13cc75 100644
--- a/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php
+++ b/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php
@@ -2448,7 +2448,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 57e23a8c64b3..1e0e8bf8ff9a 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 20c6a5e22e58..73d664455c91 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 d14af0ac934f..2f92dbee69f7 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 034d84ae0bde..6489db7f1674 100644
--- a/core/modules/media/src/MediaPermissions.php
+++ b/core/modules/media/src/MediaPermissions.php
@@ -3,6 +3,7 @@
 namespace Drupal\media;
 
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\BundlePermissionHandlerTrait;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -11,7 +12,7 @@
  * Provides dynamic permissions for each media type.
  */
 class MediaPermissions implements ContainerInjectionInterface {
-
+  use BundlePermissionHandlerTrait;
   use StringTranslationTrait;
 
   /**
@@ -47,14 +48,9 @@ public static function create(ContainerInterface $container) {
    * @see \Drupal\user\PermissionHandlerInterface::getPermissions()
    */
   public function mediaTypePermissions() {
-    $perms = [];
     // Generate media permissions for all media types.
-    $media_types = $this->entityTypeManager
-      ->getStorage('media_type')->loadMultiple();
-    foreach ($media_types as $type) {
-      $perms += $this->buildPermissions($type);
-    }
-    return $perms;
+    $media_types = $this->entityTypeManager->getStorage('media_type')->loadMultiple();
+    return $this->generatePermissions($media_types, [$this, 'buildPermissions']);
   }
 
   /**
diff --git a/core/modules/media/tests/src/Kernel/MediaTest.php b/core/modules/media/tests/src/Kernel/MediaTest.php
index 9c7ca28eef9a..93cb160e3983 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 63beba8ad7eb..c43f22c7cdb1 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 30f9ee22c82b..2ba3502caf5d 100644
--- a/core/modules/node/src/NodePermissions.php
+++ b/core/modules/node/src/NodePermissions.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\node;
 
+use Drupal\Core\Entity\BundlePermissionHandlerTrait;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\node\Entity\NodeType;
 
@@ -9,7 +10,7 @@
  * Provides dynamic permissions for nodes of different types.
  */
 class NodePermissions {
-
+  use BundlePermissionHandlerTrait;
   use StringTranslationTrait;
 
   /**
@@ -20,13 +21,7 @@ class NodePermissions {
    *   @see \Drupal\user\PermissionHandlerInterface::getPermissions()
    */
   public function nodeTypePermissions() {
-    $perms = [];
-    // Generate node permissions for all node types.
-    foreach (NodeType::loadMultiple() as $type) {
-      $perms += $this->buildPermissions($type);
-    }
-
-    return $perms;
+    return $this->generatePermissions(NodeType::loadMultiple(), [$this, 'buildPermissions']);
   }
 
   /**
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 aaf5bf2e5595..86ce7afe27b0 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 7255d3f48141..ae562cfc585d 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 000000000000..f7de6943ff06
--- /dev/null
+++ b/core/modules/rest/tests/src/Kernel/Entity/RestPermissionsTest.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Drupal\Tests\rest\Kernel\Entity;
+
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\rest\Entity\RestResourceConfig;
+use Drupal\rest\RestResourceConfigInterface;
+
+/**
+ * @coversDefaultClass \Drupal\rest\RestPermissions
+ *
+ * @group rest
+ */
+class RestPermissionsTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'rest',
+    'dblog',
+    'serialization',
+    'basic_auth',
+    'user',
+    'hal',
+  ];
+
+  /**
+   * @covers ::permissions
+   */
+  public function testPermissions() {
+    RestResourceConfig::create([
+      'id' => '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 d34d94940dc0..31aa1d614ca4 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 1b77226d17e0..68b33b2ff7ff 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';
@@ -110,6 +111,9 @@ function entity_test_entity_type_alter(array &$entity_types) {
 
   $entity_test_definition = $entity_types['entity_test'];
   $entity_test_definition->set('entity_keys', $state->get('entity_test.entity_keys', []) + $entity_test_definition->getKeys());
+
+  // Allow tests to alter the permission granularity of entity_test_mul.
+  $entity_types['entity_test_mul']->set('permission_granularity', \Drupal::state()->get('entity_test_mul.permission_granularity', 'entity_type'));
 }
 
 /**
@@ -225,7 +229,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 62e4d82b74dc..f2792ee2cf22 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 000000000000..65e58b719cb7
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulBundle.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Drupal\entity_test\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
+use Drupal\Core\Entity\EntityDescriptionInterface;
+
+/**
+ * Defines the Test entity mul bundle configuration entity.
+ *
+ * @ConfigEntityType(
+ *   id = "entity_test_mul_bundle",
+ *   label = @Translation("Test entity multilingual bundle"),
+ *   handlers = {
+ *     "access" = "\Drupal\Core\Entity\EntityAccessControlHandler",
+ *     "form" = {
+ *       "default" = "\Drupal\Core\Entity\BundleEntityFormBase",
+ *     },
+ *     "route_provider" = {
+ *       "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider",
+ *     },
+ *   },
+ *   admin_permission = "administer entity_test_mul_with_bundle content",
+ *   bundle_of = "entity_test_mul_with_bundle",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "label"
+ *   },
+ *   config_export = {
+ *     "id",
+ *     "label",
+ *     "description",
+ *   },
+ *   links = {
+ *     "add-form" = "/entity_test_mul_bundle/add",
+ *   }
+ * )
+ */
+class EntityTestMulBundle extends ConfigEntityBundleBase implements EntityDescriptionInterface {
+
+  /**
+   * The machine name.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The human-readable name.
+   *
+   * @var string
+   */
+  protected $label;
+
+  /**
+   * The description.
+   *
+   * @var string
+   */
+  protected $description;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return $this->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 000000000000..68a7e24bc6e2
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulWithBundle.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Drupal\entity_test\Entity;
+
+/**
+ * Defines the multilingual test entity class with bundles.
+ *
+ * @ContentEntityType(
+ *   id = "entity_test_mul_with_bundle",
+ *   label = @Translation("Test entity multilingual with bundle - data table"),
+ *   handlers = {
+ *     "view_builder" = "Drupal\entity_test\EntityTestViewBuilder",
+ *     "access" = "Drupal\entity_test\EntityTestAccessControlHandler",
+ *     "form" = {
+ *       "default" = "Drupal\entity_test\EntityTestForm",
+ *       "delete" = "Drupal\entity_test\EntityTestDeleteForm"
+ *     },
+ *     "translation" = "Drupal\content_translation\ContentTranslationHandler",
+ *     "views_data" = "Drupal\views\EntityViewsData",
+ *     "route_provider" = {
+ *       "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider",
+ *     },
+ *   },
+ *   base_table = "entity_test_mul_with_bundle",
+ *   data_table = "entity_test_mul_with_bundle_property_data",
+ *   admin_permission = "administer entity_test content",
+ *   translatable = TRUE,
+ *   permission_granularity = "bundle",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "uuid" = "uuid",
+ *     "bundle" = "type",
+ *     "label" = "name",
+ *     "langcode" = "langcode",
+ *   },
+ *   bundle_entity_type = "entity_test_mul_bundle",
+ *   links = {
+ *     "add-page" = "/entity_test_mul_with_bundle/add",
+ *     "add-form" = "/entity_test_mul_with_bundle/add/{type}",
+ *     "canonical" = "/entity_test_mul_with_bundle/manage/{entity_test_mul}",
+ *     "edit-form" = "/entity_test_mul_with_bundle/manage/{entity_test_mul}/edit",
+ *     "delete-form" = "/entity_test/delete/entity_test_mul_with_bundle/{entity_test_mul}",
+ *   },
+ *   field_ui_base_route = "entity.entity_test_mul_with_bundle.admin_form",
+ * )
+ */
+class EntityTestMulWithBundle extends EntityTest {
+
+}
diff --git a/core/modules/system/tests/modules/system_test/system_test.permissions.yml b/core/modules/system/tests/modules/system_test/system_test.permissions.yml
index 8faa5789e939..b51693cdfd3e 100644
--- a/core/modules/system/tests/modules/system_test/system_test.permissions.yml
+++ b/core/modules/system/tests/modules/system_test/system_test.permissions.yml
@@ -1,2 +1,5 @@
 system test:
   title: 'Administer system test'
+
+pet llamas:
+  title: 'Permission for page cache testing'
diff --git a/core/modules/system/tests/src/Functional/Rest/ActionResourceTestBase.php b/core/modules/system/tests/src/Functional/Rest/ActionResourceTestBase.php
index bfa962259177..5d233e84f8df 100644
--- a/core/modules/system/tests/src/Functional/Rest/ActionResourceTestBase.php
+++ b/core/modules/system/tests/src/Functional/Rest/ActionResourceTestBase.php
@@ -11,7 +11,7 @@ abstract class ActionResourceTestBase extends EntityResourceTestBase {
   /**
    * {@inheritdoc}
    */
-  protected static $modules = ['user'];
+  protected static $modules = ['action', 'user'];
 
   /**
    * {@inheritdoc}
@@ -70,15 +70,6 @@ protected function getExpectedNormalizedEntity() {
     ];
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function getExpectedCacheContexts() {
-    return [
-      'user.permissions',
-    ];
-  }
-
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/taxonomy/src/TaxonomyPermissions.php b/core/modules/taxonomy/src/TaxonomyPermissions.php
index c1ff5dfb07bd..054b638a7a5e 100644
--- a/core/modules/taxonomy/src/TaxonomyPermissions.php
+++ b/core/modules/taxonomy/src/TaxonomyPermissions.php
@@ -3,6 +3,7 @@
 namespace Drupal\taxonomy;
 
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\BundlePermissionHandlerTrait;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\taxonomy\Entity\Vocabulary;
@@ -14,7 +15,7 @@
  * @see taxonomy.permissions.yml
  */
 class TaxonomyPermissions implements ContainerInjectionInterface {
-
+  use BundlePermissionHandlerTrait;
   use StringTranslationTrait;
 
   /**
@@ -48,11 +49,7 @@ public static function create(ContainerInterface $container) {
    *   Permissions array.
    */
   public function permissions() {
-    $permissions = [];
-    foreach (Vocabulary::loadMultiple() as $vocabulary) {
-      $permissions += $this->buildPermissions($vocabulary);
-    }
-    return $permissions;
+    return $this->generatePermissions(Vocabulary::loadMultiple(), [$this, 'buildPermissions']);
   }
 
   /**
diff --git a/core/modules/taxonomy/tests/src/Functional/VocabularyPermissionsTest.php b/core/modules/taxonomy/tests/src/Functional/VocabularyPermissionsTest.php
index eaa802e313e2..2e4ff36ab832 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 3b95e9248d50..83f019f5af6b 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 5daf95a7c12c..74a1c410b1d7 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 d6c2ca941f13..03e30ade613a 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 46885d7e757e..4aaf8891a091 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 3512fee03e45..97b015571943 100644
--- a/core/modules/user/src/Entity/Role.php
+++ b/core/modules/user/src/Entity/Role.php
@@ -193,4 +193,72 @@ 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) {
+      $dependencies[$type] = array_keys($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;
+        // Process the next permission.
+        continue;
+      }
+
+      if (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;
+            // Process the next permission.
+            continue 2;
+          }
+        }
+      }
+    }
+
+    return $changed;
+  }
+
 }
diff --git a/core/modules/user/src/PermissionHandler.php b/core/modules/user/src/PermissionHandler.php
index dd04ab0118e7..dd568fee52a1 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 61526f339ae4..0420f564f880 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\ConfigDependencyManager
    */
   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 000000000000..fc4a8aa803db
--- /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 000000000000..fa31800661f6
--- /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 000000000000..c865a05b291c
--- /dev/null
+++ b/core/modules/user/tests/src/Functional/Update/UserUpdateRoleDependenciesTest.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Drupal\Tests\user\Functional\Update;
+
+use Drupal\FunctionalTests\Update\UpdatePathTestBase;
+use Drupal\user\Entity\Role;
+
+/**
+ * Tests user_post_update_update_roles() upgrade path.
+ *
+ * @group Update
+ * @group legacy
+ */
+class UserUpdateRoleDependenciesTest extends UpdatePathTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setDatabaseDumpFiles() {
+    $this->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 01c387dee201..bbfa5bfe22cf 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 89f272297fa3..f513f19c1cf8 100644
--- a/core/modules/user/tests/src/Kernel/UserRoleEntityTest.php
+++ b/core/modules/user/tests/src/Kernel/UserRoleEntityTest.php
@@ -7,10 +7,11 @@
 
 /**
  * @group user
+ * @coversDefaultClass \Drupal\user\Entity\Role
  */
 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 +28,22 @@ public function testOrderOfPermissions() {
     $this->assertEquals(['a', 'b', 'c'], $role->getPermissions());
   }
 
+  /**
+   * @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();
+  }
+
 }
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index 5206957f9470..7454a27f23bd 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -21,6 +21,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,17 @@ 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 the permission from any roles.
+  $permission = $filter_format->getPermissionName();
+  /** @var \Drupal\user\Entity\Role $role */
+  foreach (Role::loadMultiple() as $role) {
+    if ($role->hasPermission($permission)) {
+      $role->revokePermission($permission)->save();
+    }
+  }
+}
diff --git a/core/modules/user/user.post_update.php b/core/modules/user/user.post_update.php
index 154cd03590d2..14662df8ef01 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 cd6a27e59479..321510b054e9 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 b860296d7566..820b8454fb54 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 ac4e409555a8..8334961e2c5b 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 e0665dc1ee2e..3e30f9e01d30 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 1149947f1a29..83c4a2f22106 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 6833f166ec3b..5674329ecfb2 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 b5487dbc466e..2442711ddc8d 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/profiles/standard/config/install/user.role.content_editor.yml b/core/profiles/standard/config/install/user.role.content_editor.yml
index fddf6424bd2c..ef9426d5ec57 100644
--- a/core/profiles/standard/config/install/user.role.content_editor.yml
+++ b/core/profiles/standard/config/install/user.role.content_editor.yml
@@ -1,6 +1,21 @@
 langcode: en
 status: true
-dependencies: {  }
+dependencies:
+  config:
+    - node.type.article
+    - node.type.page
+    - taxonomy.vocabulary.tags
+  module:
+    - comment
+    - contextual
+    - file
+    - node
+    - path
+    - quickedit
+    - system
+    - taxonomy
+    - toolbar
+    - tour
 id: content_editor
 label: 'Content editor'
 weight: 2
diff --git a/core/tests/Drupal/FunctionalTests/Rest/BaseFieldOverrideResourceTestBase.php b/core/tests/Drupal/FunctionalTests/Rest/BaseFieldOverrideResourceTestBase.php
index 4a361bfb73a8..35f660eead56 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 5ccd9ad5d1f1..a2d09cff12fc 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 50f6f3b6f3e3..6d50d781bf21 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}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/BundlePermissionHandlerTraitTest.php b/core/tests/Drupal/KernelTests/Core/Entity/BundlePermissionHandlerTraitTest.php
new file mode 100644
index 000000000000..cb84b875a6d6
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/BundlePermissionHandlerTraitTest.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Core\Entity\BundlePermissionHandlerTrait;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\entity_test\Entity\EntityTestBundle;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Entity\BundlePermissionHandlerTrait
+ *
+ * @group Entity
+ */
+class BundlePermissionHandlerTraitTest extends KernelTestBase {
+  use BundlePermissionHandlerTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['entity_test'];
+
+  /**
+   * @covers ::generatePermissions
+   */
+  public function testGeneratePermissions() {
+    EntityTestBundle::create([
+      'id' => 'test1',
+    ])->save();
+    EntityTestBundle::create([
+      'id' => 'test2',
+    ])->save();
+    $permissions = $this->generatePermissions(EntityTestBundle::loadMultiple(), [$this, 'buildPermissions']);
+    $this->assertSame([
+      'title' => 'Create',
+      'dependencies' => ['config' => ['entity_test.entity_test_bundle.test1']],
+    ], $permissions['create test1']);
+    $this->assertSame([
+      'title' => 'Edit',
+      'dependencies' => [
+        'config' => [
+          'test_module.entity.test1',
+          'entity_test.entity_test_bundle.test1',
+        ],
+        'module' => ['test_module'],
+      ],
+    ], $permissions['edit test1']);
+    $this->assertSame([
+      'title' => 'Create',
+      'dependencies' => ['config' => ['entity_test.entity_test_bundle.test2']],
+    ], $permissions['create test2']);
+    $this->assertSame([
+      'title' => 'Edit',
+      'dependencies' => [
+        'config' => [
+          'test_module.entity.test2',
+          'entity_test.entity_test_bundle.test2',
+        ],
+        'module' => ['test_module'],
+      ],
+    ], $permissions['edit test2']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildPermissions(EntityInterface $bundle): array {
+    return [
+      "create {$bundle->id()}" => [
+        'title' => 'Create',
+      ],
+      "edit {$bundle->id()}" => [
+        'title' => 'Edit',
+        // Ensure it is possible for buildPermissions to add additional
+        // dependencies.
+        'dependencies' => ['config' => ["test_module.entity.{$bundle->id()}"], 'module' => ['test_module']],
+      ],
+    ];
+  }
+
+}
-- 
GitLab