Commit bcaa369c authored by catch's avatar catch
Browse files

Issue #296693 by tedbow, omkar.podey, sun, ilya.no, JeroenT, tim.plunkett,...

Issue #296693 by tedbow, omkar.podey, sun, ilya.no, JeroenT, tim.plunkett, catch, hooroomoo, ridhimaabrol24, boombatower, paulocs, Damien Tournoud, Xano, pwolanin, chx, webchick, stella, quietone, Bojhan, smustgrave, lauriii, benjifisher, gabesullice: Restrict access to empty top level administration pages
parent 09b54e97
Loading
Loading
Loading
Loading
+1 −2
Original line number Diff line number Diff line
@@ -64,8 +64,7 @@ public function testModeratedContentLocalTask() {
    // Verify the moderated content local task does not exist without the node
    // module installed.
    $this->drupalGet('admin/content');
    $this->assertSession()->statusCodeEquals(200);
    $this->assertSession()->linkNotExists('Moderated content');
    $this->assertSession()->statusCodeEquals(403);
  }

}
+1 −1
Original line number Diff line number Diff line
@@ -48,7 +48,7 @@ public function testBreadCrumbs() {
    $this->assertBreadcrumb('de/user/login', []);
    $this->assertBreadcrumb('gsw-berne/user/login', []);

    $admin_user = $this->drupalCreateUser(['access administration pages']);
    $admin_user = $this->drupalCreateUser(['access administration pages', 'administer blocks']);
    $this->drupalLogin($admin_user);

    // Use administration routes to assert that breadcrumb is displayed
+107 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\system\Access;

use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Menu\MenuLinkInterface;
use Drupal\Core\Menu\MenuLinkManagerInterface;
use Drupal\Core\Menu\MenuLinkTreeInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Routing\AccessAwareRouter;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;

/**
 * Access check for routes implementing _access_admin_menu_block_page.
 *
 * @see \Drupal\system\EventSubscriber\AccessRouteAlterSubscriber
 * @see \Drupal\system\Controller\SystemController::systemAdminMenuBlockPage()
 */
class SystemAdminMenuBlockAccessCheck implements AccessInterface {

  /**
   * Constructs a new SystemAdminMenuBlockAccessCheck.
   *
   * @param \Drupal\Core\Access\AccessManagerInterface $accessManager
   *   The access manager.
   * @param \Drupal\Core\Menu\MenuLinkTreeInterface $menuLinkTree
   *   The menu link tree service.
   * @param \Drupal\Core\Routing\AccessAwareRouter $router
   *   The router service.
   * @param \Drupal\Core\Menu\MenuLinkManagerInterface $menuLinkManager
   *   The menu link manager service.
   */
  public function __construct(
    private readonly AccessManagerInterface $accessManager,
    private readonly MenuLinkTreeInterface $menuLinkTree,
    private readonly AccessAwareRouter $router,
    private readonly MenuLinkManagerInterface $menuLinkManager,
  ) {
  }

  /**
   * Checks access.
   *
   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
   *   The cron key.
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The current user.
   *
   * @return \Drupal\Core\Access\AccessResultInterface
   *   The access result.
   */
  public function access(RouteMatchInterface $route_match, AccountInterface $account): AccessResultInterface {
    $parameters = $route_match->getParameters()->all();
    // Load links in the 'admin' menu matching this route.
    $links = $this->menuLinkManager->loadLinksByRoute($route_match->getRouteName(), $parameters, 'admin');
    if (empty($links)) {
      // If we did not find a link then we have no opinion on access.
      return AccessResult::neutral();
    }
    return $this->hasAccessToChildMenuItems(reset($links), $account)->cachePerPermissions();
  }

  /**
   * Check that the given route has access to one of it's child routes.
   *
   * @param \Drupal\Core\Menu\MenuLinkInterface $link
   *   The menu link.
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The account.
   *
   * @return \Drupal\Core\Access\AccessResultInterface
   *   The access result.
   */
  protected function hasAccessToChildMenuItems(MenuLinkInterface $link, AccountInterface $account): AccessResultInterface {
    $parameters = new MenuTreeParameters();
    $parameters->setRoot($link->getPluginId())
      ->excludeRoot()
      ->setTopLevelOnly()
      ->onlyEnabledLinks();

    $tree = $this->menuLinkTree->load(NULL, $parameters);

    if (empty($tree)) {
      $route = $this->router->getRouteCollection()->get($link->getRouteName());
      if ($route) {
        return AccessResult::allowedIf(empty($route->getRequirement('_access_admin_menu_block_page')));
      }
      return AccessResult::neutral();
    }

    foreach ($tree as $element) {
      if (!$this->accessManager->checkNamedRoute($element->link->getRouteName(), $element->link->getRouteParameters(), $account)) {
        continue;
      }

      // If access is allowed to this element in the tree check for access to
      // its own children.
      return AccessResult::allowedIf($this->hasAccessToChildMenuItems($element->link, $account)->isAllowed());
    }
    return AccessResult::neutral();
  }

}
+3 −0
Original line number Diff line number Diff line
@@ -109,6 +109,9 @@ public static function create(ContainerInterface $container) {
  /**
   * Provide the administration overview page.
   *
   * This will render child links two levels below the specified link ID,
   * grouped by the child links one level below.
   *
   * @param string $link_id
   *   The ID of the administrative path link for which to display child links.
   *
+42 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\system\EventSubscriber;

use Drupal\Core\Routing\RouteBuildEvent;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Alters routes to add necessary requirements.
 *
 * @see \Drupal\system\Access\SystemAdminMenuBlockAccessCheck
 * @see \Drupal\system\Controller\SystemController::systemAdminMenuBlockPage()
 */
class AccessRouteAlterSubscriber implements EventSubscriberInterface {

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events[RoutingEvents::ALTER][] = 'accessAdminMenuBlockPage';
    return $events;
  }

  /**
   * Adds _access_admin_menu_block_page requirement to routes pointing to SystemController::systemAdminMenuBlockPage.
   *
   * @param \Drupal\Core\Routing\RouteBuildEvent $event
   *   The event to process.
   */
  public function accessAdminMenuBlockPage(RouteBuildEvent $event) {
    $routes = $event->getRouteCollection();
    foreach ($routes as $route) {
      // Do not use a leading slash when comparing to the _controller string
      // because the leading slash in a fully-qualified method name is optional.
      if ($route->hasDefault('_controller') && ltrim($route->getDefault('_controller'), '\\') === 'Drupal\system\Controller\SystemController::systemAdminMenuBlockPage') {
        $route->setRequirement('_access_admin_menu_block_page', 'TRUE');
      }
    }
  }

}
Loading