Issue #3253955 by benjifisher, kristiaanvandeneynde, AaronMcHale: Let modules opt in to the bundle-specific permissions form

(cherry picked from commit ac9b0e681a15c82709d594719dc61944d5e67b57)
merge-requests/2247/head
Alex Pott 2022-05-09 12:19:15 +01:00
parent e169a237f9
commit 05b4b09193
No known key found for this signature in database
GPG Key ID: BDA67E7EE836E5CE
17 changed files with 217 additions and 120 deletions

View File

@ -26,7 +26,8 @@ use Drupal\block_content\BlockContentTypeInterface;
* "delete" = "Drupal\block_content\Form\BlockContentTypeDeleteForm"
* },
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider"
* "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
* "permissions" = "Drupal\user\Entity\EntityPermissionsRouteProviderWithCheck",
* },
* "list_builder" = "Drupal\block_content\BlockContentTypeListBuilder"
* },
@ -40,6 +41,7 @@ use Drupal\block_content\BlockContentTypeInterface;
* links = {
* "delete-form" = "/admin/structure/block/block-content/manage/{block_content_type}/delete",
* "edit-form" = "/admin/structure/block/block-content/manage/{block_content_type}",
* "entity-permissions-form" = "/admin/structure/block/block-content/manage/{block_content_type}/permissions",
* "collection" = "/admin/structure/block/block-content/types",
* },
* config_export = {

View File

@ -24,6 +24,9 @@ use Drupal\comment\CommentTypeInterface;
* "edit" = "Drupal\comment\CommentTypeForm",
* "delete" = "Drupal\comment\Form\CommentTypeDeleteForm"
* },
* "route_provider" = {
* "permissions" = "Drupal\user\Entity\EntityPermissionsRouteProviderWithCheck",
* },
* "list_builder" = "Drupal\comment\CommentTypeListBuilder"
* },
* admin_permission = "administer comment types",
@ -37,6 +40,7 @@ use Drupal\comment\CommentTypeInterface;
* "delete-form" = "/admin/structure/comment/manage/{comment_type}/delete",
* "edit-form" = "/admin/structure/comment/manage/{comment_type}",
* "add-form" = "/admin/structure/comment/types/add",
* "entity-permissions-form" = "/admin/structure/comment/manage/{comment_type}/permissions",
* "collection" = "/admin/structure/comment",
* },
* config_export = {

View File

@ -26,6 +26,9 @@ use Drupal\Core\Url;
* "add" = "Drupal\contact\ContactFormEditForm",
* "edit" = "Drupal\contact\ContactFormEditForm",
* "delete" = "Drupal\Core\Entity\EntityDeleteForm"
* },
* "route_provider" = {
* "permissions" = "Drupal\user\Entity\EntityPermissionsRouteProviderWithCheck",
* }
* },
* config_prefix = "form",
@ -38,6 +41,7 @@ use Drupal\Core\Url;
* links = {
* "delete-form" = "/admin/structure/contact/manage/{contact_form}/delete",
* "edit-form" = "/admin/structure/contact/manage/{contact_form}",
* "entity-permissions-form" = "/admin/structure/contact/manage/{contact_form}/permissions",
* "collection" = "/admin/structure/contact",
* "canonical" = "/contact/{contact_form}",
* },

View File

@ -30,6 +30,7 @@ use Drupal\media\MediaTypeInterface;
* "list_builder" = "Drupal\media\MediaTypeListBuilder",
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider",
* "permissions" = "Drupal\user\Entity\EntityPermissionsRouteProvider",
* }
* },
* admin_permission = "administer media types",
@ -55,6 +56,7 @@ use Drupal\media\MediaTypeInterface;
* "add-form" = "/admin/structure/media/add",
* "edit-form" = "/admin/structure/media/manage/{media_type}",
* "delete-form" = "/admin/structure/media/manage/{media_type}/delete",
* "entity-permissions-form" = "/admin/structure/media/manage/{media_type}/permissions",
* "collection" = "/admin/structure/media",
* },
* )

View File

@ -26,6 +26,9 @@ use Drupal\node\NodeTypeInterface;
* "edit" = "Drupal\node\NodeTypeForm",
* "delete" = "Drupal\node\Form\NodeTypeDeleteConfirm"
* },
* "route_provider" = {
* "permissions" = "Drupal\user\Entity\EntityPermissionsRouteProvider",
* },
* "list_builder" = "Drupal\node\NodeTypeListBuilder",
* },
* admin_permission = "administer content types",
@ -38,6 +41,7 @@ use Drupal\node\NodeTypeInterface;
* links = {
* "edit-form" = "/admin/structure/types/manage/{node_type}",
* "delete-form" = "/admin/structure/types/manage/{node_type}/delete",
* "entity-permissions-form" = "/admin/structure/types/manage/{node_type}/permissions",
* "collection" = "/admin/structure/types",
* },
* config_export = {

View File

@ -272,7 +272,7 @@ class LocalTasksTest extends BrowserTestBase {
$this->drupalGet('/admin/structure/types/manage/page');
$this->assertLocalTasks([
['entity.node_type.edit_form', ['node_type' => 'page']],
['entity.node_type.permission_form', ['node_type' => 'page']],
['entity.node_type.entity_permissions_form', ['node_type' => 'page']],
]);
// Field UI adds the usual Manage fields etc tabs.
@ -283,7 +283,7 @@ class LocalTasksTest extends BrowserTestBase {
['entity.node.field_ui_fields', ['node_type' => 'page']],
['entity.entity_form_display.node.default', ['node_type' => 'page']],
['entity.entity_view_display.node.default', ['node_type' => 'page']],
['entity.node_type.permission_form', ['node_type' => 'page']],
['entity.node_type.entity_permissions_form', ['node_type' => 'page']],
]);
}

View File

@ -31,6 +31,7 @@ use Drupal\taxonomy\VocabularyInterface;
* },
* "route_provider" = {
* "html" = "Drupal\taxonomy\Entity\Routing\VocabularyRouteProvider",
* "permissions" = "Drupal\user\Entity\EntityPermissionsRouteProvider",
* }
* },
* admin_permission = "administer taxonomy",
@ -47,6 +48,7 @@ use Drupal\taxonomy\VocabularyInterface;
* "reset-form" = "/admin/structure/taxonomy/manage/{taxonomy_vocabulary}/reset",
* "overview-form" = "/admin/structure/taxonomy/manage/{taxonomy_vocabulary}/overview",
* "edit-form" = "/admin/structure/taxonomy/manage/{taxonomy_vocabulary}",
* "entity-permissions-form" = "/admin/structure/taxonomy/manage/{taxonomy_vocabulary}/overview/permissions",
* "collection" = "/admin/structure/taxonomy",
* },
* config_export = {

View File

@ -0,0 +1,117 @@
<?php
namespace Drupal\user\Entity;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Routing\EntityRouteProviderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Provides routes for the entity permissions form.
*
* Use this class or EntityPermissionsRouteProviderWithCheck as a route
* provider for an entity type such as Vocabulary. Either one will provide
* routes for the entity permissions form. The
* EntityPermissionsRouteProviderWithCheck class provides a custom access check:
* it denies access if there are no entity-specific permissions. If you know
* that each entity has permissions, or if the check is too expensive, then use
* this class.
*/
class EntityPermissionsRouteProvider implements EntityRouteProviderInterface, EntityHandlerInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new EntityPermissionsRouteProvider.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function getRoutes(EntityTypeInterface $entity_type) {
$collection = new RouteCollection();
$entity_type_id = $entity_type->id();
if ($entity_permissions_route = $this->getEntityPermissionsRoute($entity_type)) {
$collection->add("entity.$entity_type_id.entity_permissions_form", $entity_permissions_route);
}
return $collection;
}
/**
* Gets the entity permissions route.
*
* Built only for entity types that are bundles of other entity types and
* define the 'entity-permissions-form' link template.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getEntityPermissionsRoute(EntityTypeInterface $entity_type): ?Route {
if (!$entity_type->hasLinkTemplate('entity-permissions-form')) {
return NULL;
}
if (!$bundle_of_id = $entity_type->getBundleOf()) {
return NULL;
}
$entity_type_id = $entity_type->id();
$route = new Route(
$entity_type->getLinkTemplate('entity-permissions-form'),
[
'_title' => 'Manage permissions',
'_form' => 'Drupal\user\Form\EntityPermissionsForm',
'entity_type_id' => $bundle_of_id,
'bundle_entity_type' => $entity_type_id,
],
[
'_permission' => 'administer permissions',
],
[
// Indicate that Drupal\Core\Entity\Enhancer\EntityBundleRouteEnhancer should
// set the bundle parameter.
'_field_ui' => TRUE,
'parameters' => [
$entity_type_id => [
'type' => "entity:$entity_type_id",
'with_config_overrides' => TRUE,
],
],
'_admin_route' => TRUE,
]
);
return $route;
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Drupal\user\Entity;
use Drupal\Core\Entity\EntityTypeInterface;
use Symfony\Component\Routing\Route;
/**
* Provides routes for the entity permissions form with a custom access check.
*
* Use this class or EntityPermissionsRouteProvider as a route provider for an
* entity type such as BlockContentType. Either one will provide routes for the
* entity permissions form. This class provides a custom access check: it denies
* access if there are no entity-specific permissions. If you know that each
* entity has permissions, or if the check is too expensive, then use
* EntityPermissionsRouteProvider instead of this class.
*/
class EntityPermissionsRouteProviderWithCheck extends EntityPermissionsRouteProvider {
/**
* {@inheritdoc}
*/
protected function getEntityPermissionsRoute(EntityTypeInterface $entity_type): ?Route {
$route = parent::getEntityPermissionsRoute($entity_type);
if ($route) {
$route->setRequirement('_custom_access', '\Drupal\user\Form\EntityPermissionsForm::access');
}
return $route;
}
}

View File

@ -22,7 +22,7 @@ use Symfony\Component\Routing\Route;
*
* @internal
*/
class UserPermissionsBundleForm extends UserPermissionsForm {
class EntityPermissionsForm extends UserPermissionsForm {
/**
* The configuration entity manager.
@ -46,7 +46,7 @@ class UserPermissionsBundleForm extends UserPermissionsForm {
protected $bundle;
/**
* Constructs a new UserPermissionsBundleForm.
* Constructs a new EntityPermissionsForm.
*
* @param \Drupal\user\PermissionHandlerInterface $permission_handler
* The permission handler.
@ -170,12 +170,7 @@ class UserPermissionsBundleForm extends UserPermissionsForm {
return AccessResult::forbidden();
}
$granularity = $this->entityTypeManager
->getDefinition($this->bundle->getEntityType()->getBundleOf())
->getPermissionGranularity();
$bundle_has_permissions = $granularity === 'bundle' || (bool) $this->permissionsByProvider();
return AccessResult::allowedIf($bundle_has_permissions);
return AccessResult::allowedIf((bool) $this->permissionsByProvider());
}
}

View File

@ -51,17 +51,23 @@ class UserLocalTask extends DeriverBase implements ContainerDeriverInterface {
public function getDerivativeDefinitions($base_plugin_definition) {
$this->derivatives = [];
foreach ($this->entityTypeManager->getDefinitions() as $entity_type) {
$entity_definitions = $this->entityTypeManager->getDefinitions();
foreach ($entity_definitions as $bundle_type_id => $bundle_entity_type) {
if (!$bundle_entity_type->hasLinkTemplate('entity-permissions-form')) {
continue;
}
if (!$entity_type_id = $bundle_entity_type->getBundleOf()) {
continue;
}
$entity_type = $entity_definitions[$entity_type_id];
if (!$base_route = $entity_type->get('field_ui_base_route')) {
continue;
}
if (!$bundle_entity_type = $entity_type->getBundleEntityType()) {
continue;
}
$this->derivatives["permissions_$bundle_entity_type"] = [
'route_name' => "entity.$bundle_entity_type.permission_form",
$this->derivatives["permissions_$bundle_type_id"] = [
'route_name' => "entity.$bundle_type_id.entity_permissions_form",
'weight' => 10,
'title' => $this->t('Manage permissions'),
'base_route' => $base_route,

View File

@ -1,78 +0,0 @@
<?php
namespace Drupal\user\Routing;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* User route subscriber.
*/
class RouteSubscriber extends RouteSubscriberBase {
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a RouteSubscriber object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if (!$route_name = $entity_type->get('field_ui_base_route')) {
continue;
}
if (!$bundle_entity_type = $entity_type->getBundleEntityType()) {
continue;
}
// Try to get the route from the current collection.
if (!$entity_route = $collection->get($route_name)) {
continue;
}
$route = new Route(
$entity_route->getPath() . '/permissions',
[
'_title' => 'Manage permissions',
'_form' => 'Drupal\user\Form\UserPermissionsBundleForm',
'entity_type_id' => $entity_type_id,
'bundle_entity_type' => $bundle_entity_type,
],
[
'_permission' => 'administer permissions',
'_custom_access' => '\Drupal\user\Form\UserPermissionsBundleForm::access',
],
[
// Indicate that Drupal\Core\Entity\EntityBundleRouteEnhancer should
// set the bundle parameter.
'_field_ui' => TRUE,
'parameters' => [
$bundle_entity_type => [
'type' => "entity:$bundle_entity_type",
'with_config_overrides' => TRUE,
],
],
] + $entity_route->getOptions()
);
$collection->add("entity.$bundle_entity_type.permission_form", $route);
}
}
}

View File

@ -11,7 +11,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\user\Form\UserPermissionsBundleForm;
use Drupal\user\Form\EntityPermissionsForm;
use Drupal\user\PermissionHandlerInterface;
use Drupal\user\RoleStorageInterface;
use Symfony\Component\Routing\Route;
@ -19,10 +19,10 @@ use Symfony\Component\Routing\Route;
/**
* Tests the permissions administration form for a bundle.
*
* @coversDefaultClass \Drupal\user\Form\UserPermissionsBundleForm
* @coversDefaultClass \Drupal\user\Form\EntityPermissionsForm
* @group user
*/
class UserPermissionsBundleFormTest extends UnitTestCase {
class EntityPermissionsFormTest extends UnitTestCase {
/**
* Tests generating the permissions list.
@ -73,7 +73,7 @@ class UserPermissionsBundleFormTest extends UnitTestCase {
->willReturn($entity_type);
$entity_type_manager = $prophecy->reveal();
$bundle_form = new UserPermissionsBundleForm($permission_handler, $role_storage, $module_handler, $config_manager, $entity_type_manager);
$bundle_form = new EntityPermissionsForm($permission_handler, $role_storage, $module_handler, $config_manager, $entity_type_manager);
// Mock the method parameters.
$route = new Route('some.path');

View File

@ -26,24 +26,30 @@ class UserLocalTaskTest extends UnitTestCase {
parent::setUp();
$prophecy = $this->prophesize(EntityTypeInterface::class);
$prophecy->get('field_ui_base_route')->willReturn(NULL);
$entity_no_bundle_type = $prophecy->reveal();
$prophecy->hasLinkTemplate('entity-permissions-form')->willReturn(FALSE);
$entity_no_link_template = $prophecy->reveal();
$prophecy = $this->prophesize(EntityTypeInterface::class);
$prophecy->get('field_ui_base_route')->willReturn('field_ui.base_route');
$prophecy->getBundleEntityType()->willReturn(NULL);
$entity_bundle_type = $prophecy->reveal();
$prophecy->hasLinkTemplate('entity-permissions-form')->willReturn(TRUE);
$prophecy->getBundleOf()->willReturn(NULL);
$entity_no_bundle_of = $prophecy->reveal();
$prophecy = $this->prophesize(EntityTypeInterface::class);
$prophecy->hasLinkTemplate('entity-permissions-form')->willReturn(TRUE);
$prophecy->getBundleOf()->willReturn('content_entity_type_id');
$entity_bundle_of = $prophecy->reveal();
$prophecy = $this->prophesize(EntityTypeInterface::class);
$prophecy->hasLinkTemplate('entity-permissions-form')->willReturn(FALSE);
$prophecy->get('field_ui_base_route')->willReturn('field_ui.base_route');
$prophecy->getBundleEntityType()->willReturn('field_ui_bundle_type');
$field_ui_bundle_type = $prophecy->reveal();
$content_entity_type = $prophecy->reveal();
$prophecy = $this->prophesize(EntityTypeManagerInterface::class);
$prophecy->getDefinitions()->willReturn([
'case_no_bundle_type' => $entity_no_bundle_type,
'case_bundle_type' => $entity_bundle_type,
'case_field_ui' => $field_ui_bundle_type,
'entity_no_link_template_id' => $entity_no_link_template,
'entity_no_bundle_of_id' => $entity_no_bundle_of,
'entity_bundle_of_id' => $entity_bundle_of,
'content_entity_type_id' => $content_entity_type,
]);
$entity_type_manager = $prophecy->reveal();
@ -57,8 +63,8 @@ class UserLocalTaskTest extends UnitTestCase {
*/
public function testGetDerivativeDefinitions() {
$expected = [
'permissions_field_ui_bundle_type' => [
'route_name' => 'entity.field_ui_bundle_type.permission_form',
'permissions_entity_bundle_of_id' => [
'route_name' => 'entity.entity_bundle_of_id.entity_permissions_form',
'weight' => 10,
'title' => $this->getStringTranslationStub()->translate('Manage permissions'),
'base_route' => 'field_ui.base_route',

View File

@ -0,0 +1,5 @@
# User extension relation types.
# See https://tools.ietf.org/html/rfc5988#section-4.2.
entity-permissions-form:
uri: https://drupal.org/link-relations/permissions
description: A form where bundle-related permissions can be managed.

View File

@ -1331,12 +1331,14 @@ function user_filter_format_disable(FilterFormatInterface $filter_format) {
* Implements hook_entity_operation().
*/
function user_entity_operation(EntityInterface $entity) {
// Add Manage permissions link if this entity type is the bundle of another.
if (!$bundle_entity_type = $entity->bundle()) {
// Add Manage permissions link if this entity type defines the permissions
// link template.
if (!$entity->hasLinkTemplate('entity-permissions-form')) {
return [];
}
$route = "entity.$bundle_entity_type.permission_form";
$bundle_entity_type = $entity->bundle();
$route = "entity.$bundle_entity_type.entity_permissions_form";
if (empty(\Drupal::service('router.route_provider')->getRoutesByNames([$route]))) {
return [];
}

View File

@ -52,11 +52,6 @@ services:
user.permissions:
class: Drupal\user\PermissionHandler
arguments: ['@module_handler', '@string_translation', '@controller_resolver']
user.route_subscriber:
class: Drupal\user\Routing\RouteSubscriber
arguments: ['@entity_type.manager']
tags:
- { name: event_subscriber }
user.current_user_context:
class: Drupal\user\ContextProvider\CurrentUserContext
arguments: ['@current_user', '@entity_type.manager']