Media Library + Layout Builder issue.

To select or upload a media from the media library, user requires administer block content, OR create $bundle block content,access block library.

When using media library widget from within the block content form of a layout builder inline block, where the block content has not yet been
saved
, when the user tries to click insert on the modal (select existing), or try to use the uploader, media_library.module's
MediaLibraryFieldWidgetOpener::checkAccess attempts to call createAccess which eventually gets to BlockContentAccessControlHandler::createAccess which checks for administer blocks.

In normal use of LB / Inline block user does not need this administer permission. Granting this permission is not granular enough to consider granting to lesser users.

Ultimately users should be able to select media from the library, or upload new media, without needing an administer permission. Layout builder provides a create and edit custom blocks for using inline blocks without requiring users to have administer.

Issue fork drupal-3106315

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

dpi created an issue. See original summary.

dpi’s picture

Issue summary: View changes
dpi’s picture

Issue summary: View changes

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

acbramley’s picture

It looks like this may be fixable in layout_builder_block_content_access

That hook is returning AccessResult::neutral() when there is no usages for the block yet.

komlenic’s picture

Component: media system » layout_builder.module
Priority: Normal » Major

I'm encountering this also. There is no workaround for a user who doesn't have 'administer blocks' permission, and granting that permission most often isn't a viable option, so bumping to major. This looks like a Layout Builder issue as @acbramley noted.

joao sausen’s picture

I have found the same issue. In my case i fixed it installing block_permissions.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

jastraat’s picture

It seems somewhat worthless to have the "create and edit custom blocks" permission for adding inline blocks in layout builder if users need the "administer blocks" permission to select media for that inline block.

jastraat’s picture

For reference, the error Drupal throws when an editor with the "create and edit custom blocks" permission attempts to select an image for usage in an inline block:

path: /media-library?_wrapper_format=drupal_ajax&ajax_form=1&media_library_opener_id=media_library.opener.field_widget&media_library_allowed_types%5Bimage%5D=image&media_library_selected_type=image&media_library_remaining=1&media_library_opener_parameters%5Bfield_widget_id%5D=[media reference field name]%3A-settings-block_form&media_library_opener_parameters%5Bentity_type_id%5D=block_content&media_library_opener_parameters%5Bbundle%5D=[inline block type]&media_library_opener_parameters%5Bfield_name%5D=[media reference field name]&hash=[hash]&views_display_id=widget&_wrapper_format=drupal_ajax. Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException: The 'administer blocks' permission is required. in Drupal\Core\Routing\AccessAwareRouter->checkAccess() (line 120 of /var/www/html/web/core/lib/Drupal/Core/Routing/AccessAwareRouter.php).

jastraat’s picture

I believe in addition to implementing hook_block_content_access, layout_builder needs to implement hook_block_content_create_access. We need to be able to determine in MediaLibraryFieldWidgetOpener:checkAccess if the parent form is for a regular block or an inline block. If that could be determined, it could be added to the $context array and then checked by layout_builder_block_content_create_access()

In order to determine the context of the media library, I think MediaLibraryWidget:formElement would need to be modified to add another value for the parent form ($form_state->build_info['form_id'] to $opener_parameters. If it's an inline block, the form_id would be 'layout_builder_add_block' This parameter would be available in the MediaLibraryState $state and could be checked to provide access to users with the 'create and edit custom blocks' permission if the form id matched the layout builder add form.

jastraat’s picture

As a workaround, we've added the following in a custom module:

/**
 * Implements hook_field_widget_WIDGET_TYPE_form_alter().
 */
function MODULENAME_field_widget_media_library_widget_form_alter(&$element, FormStateInterface $form_state, $context) {
  /** @var \Drupal\Core\Routing\RouteMatchInterface $route_match */
  $route_match = \Drupal::routeMatch();

  if ($route_match->getRouteName() === 'layout_builder.add_block') {
    /** @var \Drupal\media_library\MediaLibraryState $state */
    $state = $element['open_button']['#media_library_state'];
    $openerParameters = $state->getOpenerParameters();
     $openerParameters['plugin_id'] = $route_match->getParameters()->get('plugin_id');
    $new_state = MediaLibraryState::create($state->getOpenerId(), $state->getAllowedTypeIds(), $state->getSelectedTypeId(), $state->getAvailableSlots(), $openerParameters);
    $element['open_button']['#media_library_state'] = $new_state;
  }
}

/**
 * Implements hook_ENTITY_TYPE_create_access().
 */
function MODULENAME_block_content_create_access(AccountInterface $account, array $context, $entity_bundle) {
  $route_name = \Drupal::routeMatch()->getRouteName();

  if ($route_name === 'media_library.ui') {
    /** @var \Drupal\media_library\MediaLibraryState $state */
    $state = MediaLibraryState::fromRequest(\Drupal::request());
    $openerParameters = $state->getOpenerParameters();

    // If the plugin ID exists within the opener parameters, we know
    // the media library is being used on the layout builder form.
    if (isset($openerParameters['plugin_id']) && substr($openerParameters['plugin_id'], 0, 12 ) === 'inline_block') {

      if ($account->hasPermission('create and edit custom blocks')) {
        return AccessResult::allowed();
      }
    }
  }

  // No opinion.
  return AccessResult::neutral();
}

The substring check isn't really necessary right now, but in case the core media library widget were to ever add plugin_id as part of the standard $openerParameters (which might simplify media library access problems for multiple use cases), checking the plugin_id would be necessary.

randalv’s picture

Poking due to having the same issue as well.

I tried #13, and this did not solve it for me personally.

maacl’s picture

Thanks for the snippet! There is a small error:

['plugin_id'] = $route_match->getParameters()->get('plugin_id');

Should be

$openerParameters['plugin_id'] = $route_match->getParameters()->get('plugin_id');
liam morland’s picture

The fix in #13 is working for us after applying the change in #15. We also had to add some use statements to the file:

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
use Drupal\media_library\MediaLibraryState;
komlenic’s picture

Confirmed that the combined fix from #13, #15, and #16 + the addition of another use statement, does provide a workaround for this issue. The complete solution is below.

/**
 * @file
 * Put this in a custom module.
 */

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\media_library\MediaLibraryState;

/**
 * Implements hook_field_widget_WIDGET_TYPE_form_alter().
 */
function MODULENAME_field_widget_media_library_widget_form_alter(&$element, FormStateInterface $form_state, $context) {
  /** @var \Drupal\Core\Routing\RouteMatchInterface $route_match */
  $route_match = \Drupal::routeMatch();

  if ($route_match->getRouteName() === 'layout_builder.add_block') {
    /** @var \Drupal\media_library\MediaLibraryState $state */
    $state = $element['open_button']['#media_library_state'];
    $openerParameters = $state->getOpenerParameters();
    $openerParameters['plugin_id'] = $route_match->getParameters()->get('plugin_id');
    $new_state = MediaLibraryState::create($state->getOpenerId(), $state->getAllowedTypeIds(), $state->getSelectedTypeId(), $state->getAvailableSlots(), $openerParameters);
    $element['open_button']['#media_library_state'] = $new_state;
  }
}

/**
 * Implements hook_ENTITY_TYPE_create_access().
 */
function MODULENAME_block_content_create_access(AccountInterface $account, array $context, $entity_bundle) {
  $route_name = \Drupal::routeMatch()->getRouteName();

  if ($route_name === 'media_library.ui') {
    /** @var \Drupal\media_library\MediaLibraryState $state */
    $state = MediaLibraryState::fromRequest(\Drupal::request());
    $openerParameters = $state->getOpenerParameters();

    // If the plugin ID exists within the opener parameters, we know
    // the media library is being used on the layout builder form.
    if (isset($openerParameters['plugin_id']) && substr($openerParameters['plugin_id'], 0, 12) === 'inline_block') {

      if ($account->hasPermission('create and edit custom blocks')) {
        return AccessResult::allowed();
      }
    }
  }

  // No opinion.
  return AccessResult::neutral();
}

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

kriboogh’s picture

Encountered the same problem, only we also had this when we edit an existing block. To fix this I changed the form alter function to this:

/**
 * Implements hook_field_widget_WIDGET_TYPE_form_alter().
 */
function MYMODULE_field_widget_media_library_widget_form_alter(&$element, FormStateInterface $form_state, $context) {
  /** @var \Drupal\Core\Routing\RouteMatchInterface $route_match */
  $route_match = \Drupal::routeMatch();

  if (in_array($route_match->getRouteName(), ['layout_builder.add_block', 'layout_builder.update_block'])) {
    /** @var \Drupal\media_library\MediaLibraryState $state */
    $state = $element['open_button']['#media_library_state'];
    $openerParameters = $state->getOpenerParameters();
    $openerParameters['plugin_id'] = $form_state->getFormObject()->getCurrentComponent()->getPluginId();
    $new_state = MediaLibraryState::create($state->getOpenerId(), $state->getAllowedTypeIds(), $state->getSelectedTypeId(), $state->getAvailableSlots(), $openerParameters);
    $element['open_button']['#media_library_state'] = $new_state;
  }
}

The condition checks for both the add_block and update_block route.
The plugin_id is no longer fetched from the route_match, but from the component form being altered.

You also need an extra hook_ENTITY_TYPE_access implementation, for dealing with updates (same code as the create_access hook):

function MY_MODULE_block_content_access(\Drupal\Core\Entity\EntityInterface $entity, $operation, \Drupal\Core\Session\AccountInterface $account) {
  $route_name = \Drupal::routeMatch()->getRouteName();

  if ($route_name === 'media_library.ui') {
    /** @var \Drupal\media_library\MediaLibraryState $state */
    $state = MediaLibraryState::fromRequest(\Drupal::request());
    $openerParameters = $state->getOpenerParameters();

    // If the plugin ID exists within the opener parameters, we know
    // the media library is being used on the layout builder form.
    if (isset($openerParameters['plugin_id']) && substr($openerParameters['plugin_id'], 0, 12) === 'inline_block') {
      if ($account->hasPermission('create and edit custom blocks')) {
        return AccessResult::allowed();
      }
    }
  }

  // No opinion.
  return AccessResult::neutral();
}
amanire’s picture

I wonder if this is the same issue as https://www.drupal.org/project/drupal/issues/3047022?

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

akalam’s picture

I tried the workaround described on #17 and #19 and it worked for me with a small difference. In my case, \Drupal::routeMatch()->getRouteName() is returning null, but removing that check fix the access control on layout builder while the user is still forbidden on accessing to the "block layout" as expected.

kmonty’s picture

Title: Administer blocks permission required to select or upload new media with media library » Administer blocks permission required to select or upload new Media with media library when using Layout Builder

Noting that we've been using the patch mentioned in #20 #3047022: Layout builder fails to assign inline block access dependencies for the overrides section storage on entities with pending revisions for a year now. This permissions issue just cropped up for us -- updating to the latest version of the patch does not resolve the issue.

dripa’s picture

#19 didn't solve it for me. I installed and configured the block_content_permissions module.

goldin’s picture

Thanks @e.ruiter, block_content_permissions solved this for me as well.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

larowlan’s picture

Status: Active » Closed (duplicate)

#1975064: Add more granular block content permissions Would have resolved this. You just need the ability to edit the block content type the media is attached to now. This used to require administer blocks, but no longer does.

larowlan’s picture

Issue tags: +Bug Smash Initiative
acbramley’s picture

While we can use the create * block content permissions to get around this, it's a bit strange because a role doesn't need any of these permissions to actually manage block content inside layout builder at all.

This means we need to give roles create permissions for blocks they can create in layout builder, only for media library access. But then they'd have access to create them via block/add/foo as well? Leaning on the side of reopening this one.

acbramley’s picture

Title: Administer blocks permission required to select or upload new Media with media library when using Layout Builder » Block content permissions required to select or upload new Media with media library when using Layout Builder
Issue summary: View changes
Status: Closed (duplicate) » Active

Yeah, you also need "access block library" which again may not be desirable.

luke.leber’s picture

I agree with #31 here.

Our particular use case is that we only want a higher echelon of users to be able to create / edit / delete reusable blocks (due to how far-reaching their usage might be), while allowing lower users to manage inline blocks.

As it stands, even with contrib module helpers, users still seem to need create permission on the block type in order to use the media library for inline blocks. This comes with the nasty side-effect of allowing them to create reusable blocks as well.

johnpitcairn’s picture

I also agree with #31.

Content editors who should only interact with blocks via layout builder should not see the reusable blocks overview page or be able to create standalone reusable blocks from there.

ammaletu’s picture

We just stumbled upon a new variant of this bug (Drupal 10.1.7): Our users have the "administer blocks" permission, but not the new per content block permissions nor the "Access the Content blocks overview page" permission. Everything worked fine for them with Drupal 9. Since the update, they are not able to add media to a block anymore.

The error message which is logged when clicking on the "Insert" button in the media modal (no error shown to the user):

Path: [URL with many parameters]. Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException: The following permissions are required: 'create gallery_widget block content' AND 'access block library'. in Drupal\Core\Routing\AccessAwareRouter->checkAccess() (line 118 of /var/www/html/web/core/lib/Drupal/Core/Routing/AccessAwareRouter.php).

The change record for the new permissions sounded as if they were optional, as long as users have the "administer blocks" permission. Now it seems, this has changed so that you do need the new permissions for adding media items to a block!?

ian.ssu made their first commit to this issue’s fork.

ian.ssu’s picture

I spent an entire day with the debugger trying to find an elegant solution to this issue. My conclusion is that LayoutBuilder InlineBlock, by adding the Reusable property to BlockContentEntity, creates a separation of concerns problem. This approach couples BlockContent to LayoutBuilder in ways that other modules, like MediaLibrary, shouldn't have to worry about. I've started considering extending BlockContentEntity for InlineBlock as a potential path toward a solution.

Unable to come up with a quick solution, I pivoted back to the workaround for Drupal 10.2.6
https://git.drupalcode.org/issue/drupal-3106315/-/compare/11.x...3106315...

fenstrat’s picture

Issue tags: +Needs tests

The work around in #37 works well. It can also be implemented in a custom module to the same effect.
It's essentially a simplified version of the fixes others have used above.

I'd agree that this is an unfortunate separation of concerns issue. So, as the 'create and edit custom blocks' permissions seems to cover the cases where media is not used on a block, then re-using that permission where media is used seems like a valid work around.

chrisgross’s picture

I'm running into a similar problem on 10.3.2. However, the suggested solution does not work for me, though my use case is perhaps slightly different.

I am using Block Content Permissions (for some reason core's newer Block Content permissions do not list "create" permissions at all on my site) , but I have chosen not to give any roles "Create new block content" permissions for my various custom block types and this is preventing media from being uploaded to fields on those blocks in Layout Builder. Granting those permissions does fix that and allow media to be added to the library within Layout Builder blocks, however, it also allows users to add new instances of these custom block types under '/block/add', which I do not want. I only want those users to be able to add those blocks in Layout Builder.

The "Create new block content" permissions are not required in order to actually add these custom blocks in Layout Builder at all (which I believe is how it's supposed to work), but they are required in order to add media to fields within those blocks. So I believe this is a variation of this same problem, which is that Layout Builder is using faulty access checks when uploading media to custom blocks.

I'm not sure why the solution in #37 does not work for me.

Update: I wonder if this is because core's newer Block Content permissions somehow left out Create permissions entirely, but it doesn't seem like the fix is considered urgent enough to even make it into 10.4: #3412420: BlockContentAccessControlHandler requires access block library permission for create. So maybe there is really no way to know how any of this is going to work until that fix is merged in and we are able to fully transition away from the Block Content Permissions contrib?

jastraat’s picture

Updated work around for 10.4.x:

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Form\FormStateInterface;
use Drupal\media_library\MediaLibraryState;

/**
 * Implements hook_field_widget_single_element_WIDGET_TYPE_form_alter().
 * 
 * Add the plugin ID to the media library state when in layout builder.
 */
function MODULE_field_widget_single_element_media_library_widget_form_alter(array &$element, FormStateInterface $form_state, array $context) {
  $route_match = \Drupal::routeMatch();

  if (in_array($route_match->getRouteName(), ['layout_builder.add_block', 'layout_builder.update_block'])) {
    /** @var \Drupal\media_library\MediaLibraryState $state */
    $state = $element['open_button']['#media_library_state'];
    $openerParameters = $state->getOpenerParameters();
    $openerParameters['plugin_id'] = $form_state->getFormObject()->getCurrentComponent()->getPluginId();
    $new_state = MediaLibraryState::create($state->getOpenerId(), $state->getAllowedTypeIds(), $state->getSelectedTypeId(), $state->getAvailableSlots(), $openerParameters);
    $element['open_button']['#media_library_state'] = $new_state;
  }
}

/**
 * Implements hook_ENTITY_TYPE_create_access().
 * 
 * Allow editors with inline block permissions to insert from media library.
 */
function MODULE_block_content_create_access(AccountInterface $account, array $context, $entity_bundle) {
  $route_name = \Drupal::routeMatch()->getRouteName();

  if ($route_name === 'media_library.ui') {
    /** @var \Drupal\media_library\MediaLibraryState $state */
    $state = MediaLibraryState::fromRequest(\Drupal::request());
    $openerParameters = $state->getOpenerParameters();

    // If the plugin ID exists within the opener parameters, we know
    // the media library is being used on the layout builder form.
    if (isset($openerParameters['plugin_id']) && str_starts_with($openerParameters['plugin_id'], 'inline_block')) {

      if ($account->hasPermission('create and edit custom blocks')) {
        return AccessResult::allowed();
      }
    }
  }

  // No opinion.
  return AccessResult::neutral();
}

erik.erskine’s picture

Component: layout_builder.module » media system
Related issues: +#3327106: MediaLibraryFieldWidgetOpener is too opinionated

The problem really lies with media_library expecting the create {bundle} block content permission. This is described in #3327106: MediaLibraryFieldWidgetOpener is too opinionated.

Layout builder doesn't require you have that permission in order to create inline content blocks. Pretending it does under certain circumstances feels like the wrong approach. It also seems strange that the result of EntityAccessControlHandlerInterface::createAccess() would vary per route.

Interestingly, the exact same problem is manifested in the group module (#3071489: Incorrect Access Check on Media Library). This is a very similar scenario: you are allowed to create nodes nodes, but only in a restricted way (as part of a group), and without having full on create {bundle} content permission.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.