Commit 687cf58d authored by webchick's avatar webchick
Browse files

Issue #2914486 by tim.plunkett, tedbow, bendeguz.csirmaz, twfahey, bnjmnm,...

Issue #2914486 by tim.plunkett, tedbow, bendeguz.csirmaz, twfahey, bnjmnm, xjm, effulgentsia, Kristen Pol, benjifisher, larowlan, webchick, rainbreaw, jrockowitz, Gábor Hojtsy, ckrina: Add granular permissions to the Layout Builder

(cherry picked from commit d05d67fe)
parent d0ec4df0
<?php
/**
* @file
* Hooks provided by the Layout Builder module.
*/
/**
* @defgroup layout_builder_access Layout Builder access
* @{
* In determining access rights for the Layout Builder UI,
* \Drupal\layout_builder\Access\LayoutBuilderAccessCheck checks if the
* specified section storage plugin (an implementation of
* \Drupal\layout_builder\SectionStorageInterface) grants access.
*
* By default, the Layout Builder access check requires the 'configure any
* layout' permission. Individual section storage plugins may override this by
* setting the 'handles_permission_check' annotation key to TRUE. Any section
* storage plugin that uses 'handles_permission_check' must provide its own
* complete routing access checking to avoid any access bypasses.
*
* This access checking is only enforced on the routing level (not on the entity
* or field level) with additional form access restrictions. All HTTP API access
* to Layout Builder data is currently forbidden.
*
* @see https://www.drupal.org/project/drupal/issues/2942975
*/
/**
* @} End of "defgroup layout_builder_access".
*/
......@@ -56,6 +56,11 @@ function layout_builder_help($route_name, RouteMatchInterface $route_match) {
$output .= '<dd>' . t('Layout Builder can be selectively enabled on the "Manage Display" page in the <a href=":field_ui">Field UI</a>. This allows you to control the output of each type of display individually. For example, a "Basic page" might have view modes such as Full and Teaser, with each view mode having different layouts selected.', [':field_ui' => Url::fromRoute('help.page', ['name' => 'field_ui'])->toString()]) . '</dd>';
$output .= '<dt>' . t('Overridden layouts') . '</dt>';
$output .= '<dd>' . t('If enabled, each individual content item can have a custom layout. Once the layout for an individual content item is overridden, changes to the Default layout will no longer affect it. Overridden layouts may be reverted to return to matching and being synchronized to their Default layout.') . '</dd>';
$output .= '<dt>' . t('User permissions') . '</dt>';
$output .= '<dd>' . t('The Layout Builder module makes a number of permissions available, which can be set by role on the <a href=":permissions">permissions page</a>. For more information, see the <a href=":layout-builder-permissions">Configuring Layout Builder permissions</a> online documentation.', [
':permissions' => Url::fromRoute('user.admin_permissions', [], ['fragment' => 'module-layout_builder'])->toString(),
':layout-builder-permissions' => 'https://www.drupal.org/docs/8/core/modules/layout-builder/configuring-layout-builder-permissions',
]) . '</dd>';
$output .= '</dl>';
return $output;
}
......@@ -258,7 +263,7 @@ function layout_builder_block_content_access(EntityInterface $entity, $operation
return AccessResult::neutral();
}
if ($account->hasPermission('configure any layout')) {
if ($account->hasPermission('create and edit custom blocks')) {
return AccessResult::allowed();
}
return AccessResult::forbidden();
......
# @todo Expand permissions to be more granular in
# https://www.drupal.org/node/2914486.
configure any layout:
title: 'Configure any layout'
restrict access: true
create and edit custom blocks:
title: 'Create and edit custom blocks'
description: 'Manage the single-use blocks within the Layout Builder'
permission_callbacks:
- \Drupal\layout_builder\LayoutBuilderOverridesPermissions::permissions
......@@ -8,6 +8,7 @@
use Drupal\Core\Config\Entity\ConfigEntityUpdater;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
use Drupal\layout_builder\TempStoreIdentifierInterface;
use Drupal\user\Entity\Role;
/**
* Rebuild plugin dependencies for all entity view displays.
......@@ -174,3 +175,14 @@ function layout_builder_post_update_section_third_party_settings_schema() {
function layout_builder_post_update_layout_builder_dependency_change() {
// Empty post-update hook.
}
/**
* Add new custom block permission to all roles with 'configure any layout'.
*/
function layout_builder_post_update_update_permissions() {
foreach (Role::loadMultiple() as $role) {
if ($role->hasPermission('configure any layout')) {
$role->grantPermission('create and edit custom blocks')->save();
}
}
}
......@@ -4,7 +4,6 @@ layout_builder.choose_section:
_controller: '\Drupal\layout_builder\Controller\ChooseSectionController::build'
_title: 'Choose a layout for this section'
requirements:
_permission: 'configure any layout'
_layout_builder_access: 'view'
options:
_admin_route: TRUE
......@@ -17,7 +16,6 @@ layout_builder.add_section:
defaults:
_controller: '\Drupal\layout_builder\Controller\AddSectionController::build'
requirements:
_permission: 'configure any layout'
_layout_builder_access: 'view'
options:
_admin_route: TRUE
......@@ -34,7 +32,6 @@ layout_builder.configure_section:
# section does not.
plugin_id: null
requirements:
_permission: 'configure any layout'
_layout_builder_access: 'view'
options:
_admin_route: TRUE
......@@ -47,7 +44,6 @@ layout_builder.remove_section:
defaults:
_form: '\Drupal\layout_builder\Form\RemoveSectionForm'
requirements:
_permission: 'configure any layout'
_layout_builder_access: 'view'
options:
_admin_route: TRUE
......@@ -61,7 +57,6 @@ layout_builder.choose_block:
_controller: '\Drupal\layout_builder\Controller\ChooseBlockController::build'
_title: 'Choose a block'
requirements:
_permission: 'configure any layout'
_layout_builder_access: 'view'
options:
_admin_route: TRUE
......@@ -75,7 +70,6 @@ layout_builder.add_block:
_form: '\Drupal\layout_builder\Form\AddBlockForm'
_title: 'Configure block'
requirements:
_permission: 'configure any layout'
_layout_builder_access: 'view'
options:
_admin_route: TRUE
......@@ -89,7 +83,7 @@ layout_builder.choose_inline_block:
_controller: '\Drupal\layout_builder\Controller\ChooseBlockController::inlineBlockList'
_title: 'Add a new Inline Block'
requirements:
_permission: 'configure any layout'
_layout_builder_access: 'view'
options:
_admin_route: TRUE
parameters:
......@@ -102,7 +96,6 @@ layout_builder.update_block:
_form: '\Drupal\layout_builder\Form\UpdateBlockForm'
_title: 'Configure block'
requirements:
_permission: 'configure any layout'
_layout_builder_access: 'view'
options:
_admin_route: TRUE
......@@ -116,8 +109,6 @@ layout_builder.move_block_form:
_title_callback: '\Drupal\layout_builder\Form\MoveBlockForm::title'
_form: '\Drupal\layout_builder\Form\MoveBlockForm'
requirements:
# @todo revisit in https://www.drupal.org/node/2914486
_permission: 'configure any layout'
_layout_builder_access: 'view'
options:
_admin_route: TRUE
......@@ -130,7 +121,6 @@ layout_builder.remove_block:
defaults:
_form: '\Drupal\layout_builder\Form\RemoveBlockForm'
requirements:
_permission: 'configure any layout'
_layout_builder_access: 'view'
options:
_admin_route: TRUE
......@@ -149,7 +139,6 @@ layout_builder.move_block:
block_uuid: null
preceding_block_uuid: null
requirements:
_permission: 'configure any layout'
_layout_builder_access: 'view'
options:
_admin_route: TRUE
......
......@@ -7,6 +7,9 @@ services:
tags:
- { name: access_check, applies_to: _layout_builder_access }
access_check.entity.layout:
# Deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use
# access_check.entity.layout_builder_access instead. See
# https://www.drupal.org/node/3039551.
class: Drupal\layout_builder\Access\LayoutSectionAccessCheck
tags:
- { name: access_check, applies_to: _has_layout_section }
......
......@@ -2,6 +2,7 @@
namespace Drupal\layout_builder\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
......@@ -11,6 +12,8 @@
/**
* Provides an access check for the Layout Builder defaults.
*
* @ingroup layout_builder_access
*
* @internal
*/
class LayoutBuilderAccessCheck implements AccessInterface {
......@@ -31,6 +34,13 @@ class LayoutBuilderAccessCheck implements AccessInterface {
public function access(SectionStorageInterface $section_storage, AccountInterface $account, Route $route) {
$operation = $route->getRequirement('_layout_builder_access');
$access = $section_storage->access($operation, $account, TRUE);
// Check for the global permission unless the section storage checks
// permissions itself.
if (!$section_storage->getPluginDefinition()->get('handles_permission_check')) {
$access = $access->andIf(AccessResult::allowedIfHasPermission($account, 'configure any layout'));
}
if ($access instanceof RefinableCacheableDependencyInterface) {
$access->addCacheableDependency($section_storage);
}
......
......@@ -12,6 +12,10 @@
* Provides an access check for the Layout Builder UI.
*
* @internal
*
* @todo Deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use
* \Drupal\layout_builder\Access\LayoutBuilderAccessCheck instead. See
* https://www.drupal.org/node/3039551.
*/
class LayoutSectionAccessCheck implements AccessInterface {
......@@ -27,6 +31,7 @@ class LayoutSectionAccessCheck implements AccessInterface {
* The access result.
*/
public function access(RouteMatchInterface $route_match, AccountInterface $account) {
@trigger_error(__NAMESPACE__ . '\LayoutSectionAccessCheck is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use \Drupal\layout_builder\Access\LayoutBuilderAccessCheck instead. See https://www.drupal.org/node/3039551.', E_USER_DEPRECATED);
$section_storage = $route_match->getParameter('section_storage');
if (empty($section_storage)) {
......
......@@ -46,6 +46,20 @@ class SectionStorage extends Plugin {
*/
public $context_definitions = [];
/**
* Indicates that this section storage handles its own permission checking.
*
* If FALSE, the 'configure any layout' permission will be required during
* routing access. If TRUE, Layout Builder will not enforce any access
* restrictions for the storage, so the section storage's implementation of
* access() must perform the access checking itself. Defaults to FALSE.
*
* @var bool
*
* @see \Drupal\layout_builder\Access\LayoutBuilderAccessCheck
*/
public $handles_permission_check = FALSE;
/**
* {@inheritdoc}
*/
......
......@@ -6,6 +6,7 @@
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
......@@ -39,6 +40,13 @@ class ChooseBlockController implements ContainerInjectionInterface {
*/
protected $entityTypeManager;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* ChooseBlockController constructor.
*
......@@ -46,10 +54,17 @@ class ChooseBlockController implements ContainerInjectionInterface {
* The block manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(BlockManagerInterface $block_manager, EntityTypeManagerInterface $entity_type_manager) {
public function __construct(BlockManagerInterface $block_manager, EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user = NULL) {
$this->blockManager = $block_manager;
$this->entityTypeManager = $entity_type_manager;
if (!$current_user) {
@trigger_error('The current_user service must be passed to ChooseBlockController::__construct(), it is required before Drupal 9.0.0.', E_USER_DEPRECATED);
$current_user = \Drupal::currentUser();
}
$this->currentUser = $current_user;
}
/**
......@@ -58,7 +73,8 @@ public function __construct(BlockManagerInterface $block_manager, EntityTypeMana
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.block'),
$container->get('entity_type.manager')
$container->get('entity_type.manager'),
$container->get('current_user')
);
}
......@@ -106,6 +122,7 @@ public function build(SectionStorageInterface $section_storage, $delta, $region)
'@entity_type' => $this->entityTypeManager->getDefinition('block_content')->getSingularLabel(),
]),
'#attributes' => $this->getAjaxAttributes(),
'#access' => $this->currentUser->hasPermission('create and edit custom blocks'),
];
$build['add_block']['#attributes']['class'][] = 'inline-block-create-button';
}
......
......@@ -77,7 +77,9 @@ public function equals(FieldItemListInterface $list_to_compare) {
}
/**
* {@inheritdoc}
* Overrides \Drupal\Core\Field\FieldItemListInterface::defaultAccess().
*
* @ingroup layout_builder_access
*/
public function defaultAccess($operation = 'view', AccountInterface $account = NULL) {
// @todo Allow access in https://www.drupal.org/node/2942975.
......
<?php
namespace Drupal\layout_builder;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides dynamic permissions for Layout Builder overrides.
*
* @see \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage::access()
*/
class LayoutBuilderOverridesPermissions implements ContainerInjectionInterface {
use StringTranslationTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity type bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $bundleInfo;
/**
* LayoutBuilderOverridesPermissions constructor.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
* The bundle info service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_info) {
$this->entityTypeManager = $entity_type_manager;
$this->bundleInfo = $bundle_info;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('entity_type.bundle.info')
);
}
/**
* Returns an array of permissions.
*
* @return string[][]
* An array whose keys are permission names and whose corresponding values
* are defined in \Drupal\user\PermissionHandlerInterface::getPermissions().
*/
public function permissions() {
$permissions = [];
/** @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface[] $entity_displays */
$entity_displays = $this->entityTypeManager->getStorage('entity_view_display')->loadByProperties(['third_party_settings.layout_builder.allow_custom' => TRUE]);
foreach ($entity_displays as $entity_display) {
$entity_type_id = $entity_display->getTargetEntityTypeId();
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
$bundle = $entity_display->getTargetBundle();
$args = [
'%entity_type' => $entity_type->getCollectionLabel(),
'@entity_type_singular' => $entity_type->getSingularLabel(),
'@entity_type_plural' => $entity_type->getPluralLabel(),
'%bundle' => $this->bundleInfo->getBundleInfo($entity_type_id)[$bundle]['label'],
];
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),
];
$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),
];
}
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),
];
$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),
];
}
}
return $permissions;
}
}
......@@ -61,6 +61,13 @@ class InlineBlock extends BlockBase implements ContainerFactoryPluginInterface,
*/
protected $isNew = TRUE;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a new InlineBlock.
*
......@@ -74,8 +81,10 @@ class InlineBlock extends BlockBase implements ContainerFactoryPluginInterface,
* The entity type manager service.
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
* The entity display repository.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityDisplayRepositoryInterface $entity_display_repository) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityDisplayRepositoryInterface $entity_display_repository, AccountInterface $current_user = NULL) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
......@@ -83,6 +92,12 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
if (!empty($this->configuration['block_revision_id']) || !empty($this->configuration['block_serialized'])) {
$this->isNew = FALSE;
}
if (!$current_user) {
@trigger_error('The current_user service must be passed to InlineBlock::__construct(), it is required before Drupal 9.0.0.', E_USER_DEPRECATED);
$current_user = \Drupal::currentUser();
}
$this->currentUser = $current_user;
}
/**
......@@ -94,7 +109,8 @@ public static function create(ContainerInterface $container, array $configuratio
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('entity_display.repository')
$container->get('entity_display.repository'),
$container->get('current_user')
);
}
......@@ -121,6 +137,7 @@ public function blockForm($form, FormStateInterface $form_state) {
'#type' => 'container',
'#process' => [[static::class, 'processBlockForm']],
'#block' => $block,
'#access' => $this->currentUser->hasPermission('create and edit custom blocks'),
];
$options = $this->entityDisplayRepository->getViewModeOptionsByBundle('block_content', $block->bundle());
......
......@@ -422,7 +422,7 @@ public function getThirdPartyProviders() {
* {@inheritdoc}
*/
public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
$result = AccessResult::allowedIf($this->isLayoutBuilderEnabled());
$result = AccessResult::allowedIf($this->isLayoutBuilderEnabled())->addCacheableDependency($this);
return $return_as_object ? $result : $result->isAllowed();
}
......
......@@ -34,6 +34,7 @@
* @SectionStorage(
* id = "overrides",
* weight = -20,
* handles_permission_check = TRUE,
* context_definitions = {
* "entity" = @ContextDefinition("entity", constraints = {
* "EntityHasField" = \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage::FIELD_NAME,
......@@ -84,16 +85,28 @@ class OverridesSectionStorage extends SectionStorageBase implements ContainerFac
*/
protected $entityRepository;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, SectionStorageManagerInterface $section_storage_manager, EntityRepositoryInterface $entity_repository) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, SectionStorageManagerInterface $section_storage_manager, EntityRepositoryInterface $entity_repository, AccountInterface $current_user = NULL) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
$this->sectionStorageManager = $section_storage_manager;
$this->entityRepository = $entity_repository;
if (!$current_user) {
@trigger_error('The current_user service must be passed to OverridesSectionStorage::__construct(), it is required before Drupal 9.0.0.', E_USER_DEPRECATED);
$current_user = \Drupal::currentUser();
}
$this->currentUser = $current_user;
}
/**
......@@ -107,7 +120,8 @@ public static function create(ContainerInterface $container, array $configuratio
$container->get('entity_type.manager'),
$container->get('entity_field.manager'),
$container->get('plugin.manager.layout_builder.section_storage'),
$container->get('entity.repository')
$container->get('entity.repository'),
$container->get('current_user')
);
}
......@@ -360,8 +374,29 @@ public function save() {
* {@inheritdoc}
*/
public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
$default_section_storage = $this->getDefaultSectionStorage();
$result = AccessResult::allowedIf($default_section_storage->isLayoutBuilderEnabled())->addCacheableDependency($default_section_storage);
if ($account === NULL) {
$account = $this->currentUser;
}
$entity = $this->getEntity();
// Create an access result that will allow access to the layout if one of
// these conditions applies:
// 1. The user can configure any layouts.
$any_access = AccessResult::allowedIfHasPermission($account, 'configure any layout');
// 2. The user can configure layouts on all items of the bundle type.
$bundle_access = AccessResult::allowedIfHasPermission($account, "configure all {$entity->bundle()} {$entity->getEntityTypeId()} layout overrides");
// 3. The user can configure layouts items of this bundle type they can edit
// AND the user has access to edit this entity.
$edit_only_bundle_access = AccessResult::allowedIfHasPermission($account, "configure editable {$entity->bundle()} {$entity->getEntityTypeId()} layout overrides");
$edit_only_bundle_access = $edit_only_bundle_access->andIf($entity->access('update', $account, TRUE));
$result = $any_access
->orIf($bundle_access)
->orIf($edit_only_bundle_access);
// Access also depends on the default being enabled.
$result = $result->andIf($this->getDefaultSectionStorage()->access($operation, $account, TRUE));
return $return_as_object ? $result : $result->isAllowed();
}
......
......@@ -45,7 +45,6 @@ protected function buildLayoutRoutes(RouteCollection $collection, SectionStorage
// Provide an empty value to allow the section storage to be upcast.
$defaults['section_storage'] = '';
// Trigger the layout builder access check.
$requirements['_has_layout_section'] = 'true';
$requirements['_layout_builder_access'] = 'view';
// Trigger the layout builder RouteEnhancer.
$options['_layout_builder'] = TRUE;
......
......@@ -6,6 +6,7 @@
use Drupal\Core\Access\AccessibleInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\Routing\RouteCollection;
<