Why does Route access checking differ from Entity access checking?

Created on 9 August 2018, over 6 years ago
Updated 26 March 2024, 8 months ago

Today, I had to solve a problem which boggled my mind. I added a custom AccessCheck to the route 'entity.node.canonical', on top of the existing access check, and made it return AccessResult::neutral().

I was very confused to find that the AccessResult::allowed() from the original node check, and my AccessResult::neutral(), combined into a final AccessResult::neutral() which made the route throw a 403 Access Denied error.

I looked deeper into it and investigated the AccessResult::andIf() method that is used to combine these access checks.

The three big "if" cases of the method are:

isForbidden() in either ⇒ isForbidden()
otherwise, if isAllowed() in both ⇒ isAllowed()
otherwise, one of them is isNeutral() ⇒ isNeutral()

So, I feel like there's a logic problem here.

If "isNeutral()" is meant to be the "I don't care what happens with the access" response, why does it override "isAllowed()"?

Shouldn't the if statement

elseif ($this->isAllowed() && $other->isAllowed()) {
    $result = static::allowed();
    $merge_other = TRUE;
}

instead be...

elseif ($this->isAllowed() || $other->isAllowed()) {

to make logical sense?

This differentiation is already documented in this article , but it doesn't provide an explanation for why this happens.

🐛 Bug report
Status

Needs work

Version

11.0 🔥

Component
Routing 

Last updated 1 day ago

Created by

🇩🇪Germany yoruvo Stuttgart

Live updates comments and jobs are added and updated live.
  • DrupalWTF

    Worse Than Failure. Approximates the unpleasant remark made by Drupal developers when they first encounter a particular (mis)feature.

  • Needs issue summary update

    Issue summaries save everyone time if they are kept up-to-date. See Update issue summary task instructions.

Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • 🇬🇧United Kingdom Dubs

    Ditto with Group and some of these operations. Applying the patch is not recommended without very carefully checking your module configuration.

  • 🇺🇸United States maskedjellybean Portland, OR

    Solving this is far beyond me, but I've struggled with these inconsistencies and would love to see a solution.

  • 🇨🇭Switzerland amermod

    I made a patch for the solution proposed in #27.
    I think it's a good solution for BC and is easy to implement for everyone who needs it.

    Then we can add a deprecation notice and switch for the Symfony access check in a future Drupal release maybe ?

  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    45:02
    44:49
    Running
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    30,322 pass
  • 🇨🇦Canada tolu_

    Use $access_result->isForbidden() explicitly since Access::Neutral() is also not $access_result->isAllowed()

  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7 updated deps
    last update about 1 year ago
    28,174 pass, 907 fail
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7 updated deps
    last update about 1 year ago
    28,185 pass, 907 fail
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MariaDB 10.3.22
    last update about 1 year ago
    28,185 pass, 907 fail
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7 updated deps
    58:46
    57:52
    Running
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7 updated deps
    last update about 1 year ago
    Custom Commands Failed
  • 🇺🇸United States DamienMcKenna NH, USA

    Patch #44 resolves the problem for me.

    I just ran into this problem using Edit Media Modal - users who have permission to edit the media object have AccessResultNeutral after the access checks are done, which leads to core failing the check instead of allowing it.

  • 🇺🇸United States DamienMcKenna NH, USA

    The test failures imply that a lot of core's permission logic does not return Forbidden but instead return Neutral, which the old logic took to mean "no" when it should have been "yes".

  • 🇺🇸United States DamienMcKenna NH, USA

    With #44 the /user/logout route redirects to /user, but for some reason it then redirects to /user/0 instead of /user/login, and then gets a 403 error. This seems to cause most of the test failures.

    This issue highlights two major DX problems:
    1. A lack of documentation around access logic, how it should be used and how modules should interpret it.
    2. Limitations in core's access tests, which leads people to thinking that certain operations should work, when they don't.

    The problem I ran into with edit_media_modal is a perfect example of this - the user has the "edit [media type]" permission, the contrib logic uses that permission to determine access, but the access check fails when the user should have the appropriate access.

  • 🇨🇦Canada Charlie ChX Negyesi 🍁Canada

    From #42 it would seem #27 was not clear: I suggested _symfony_routing_access to indicate a route is using symfony/Security instead of Drupal's system. I did not suggest this flag to change the behavior of Drupal's system.

    I also believe both #42 and #44 needs some consider whether whether generic modules changing route access would open security holes with them.

    For small changes I think #14 was the only feasible one. But, of course, what do I know? This is just my thought.

    For large changes you would need to bring in symfony/security.

  • 🇩🇪Germany drubb Sindelfingen

    Just to summarize the current situation in other words, hopefully not getting it wrong:

    Route Access is allowed, if ALL access checks return AccessResult::allowed(). It’s denied if ANY access check does NOT return AccessResult::allowed().

    Entity Access is allowed, if ANY access check returns AccessResult:allowed() and NO access check returns AccessResult::forbidden(). It’s denied if NO access check returns AccessResult::allowed() or ANY access check returns AccessResult::forbidden().

  • 🇧🇪Belgium kristiaanvandeneynde Antwerp, Belgium

    Re #49 Sounds about right. I also summarized it as such in #33

  • 🇬🇧United Kingdom joachim
  • 🇳🇱Netherlands Ruuds

    The patch in #44 allows anonymous users to edit menus (/admin/structure/menu/manage/*), so beware of that!

Production build 0.71.5 2024