How to prevent caching of AccessResults when orIf() is involved?

Created on 13 February 2023, over 1 year ago
Updated 11 June 2024, 4 months ago

Problem/Motivation

(This is a follow up a Slack thread where I tried to figure out whether the current behavior of AccessResult::orIf() is expected or not).

Can somebody explain in which circumstances this statement should be valid on \Drupal\Core\Access\AccessResult::inheritCacheability()?

* inheritCacheability() differs from addCacheableDependency() in how it
* handles max-age, because it is designed to inherit the cacheability of the
* second operand in the andIf() and orIf() operations. There, the situation
* "allowed, max-age=0 OR allowed, max-age=1000" needs to yield max-age 1000
* as the end result.

That comment was added to the code here: https://www.drupal.org/project/drupal/issues/2526326#comment-10153264 β†’
I am asking because I am hunting down the root cause of a weird caching issue where in the entity access layer AccessResult::allowed()->setMaxAge(0) set by an access hook had become a result that can be permanently cached just because it was orIf-ed with AccessResult::allowed() . You can imagine that killing cache was set by a reason because the access result depends on something that cannot be invalidated by cache tags or vary per cache context. This behavior also has security implications of course.

Steps to reproduce

vendor/bin/drush eval "(assert(0 == \Drupal\Core\Access\AccessResult::allowed()->setCacheMaxAge(0)->orIf(\Drupal\Core\Access\AccessResult::allowed())->getCacheMaxAge()));""

Proposed resolution

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

πŸ’¬ Support request
Status

Closed: works as designed

Version

11.0 πŸ”₯

Component
BaseΒ  β†’

Last updated about 3 hours ago

Created by

πŸ‡­πŸ‡ΊHungary mxr576 Hungary

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

Not all content is available!

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

  • Issue created by @mxr576
  • πŸ‡­πŸ‡ΊHungary mxr576 Hungary

    Okay, so from this nested IF condition, the following triggers the merge $this->getCacheMaxAge() === 0 && $other->isAllowed() . Which should be the case...

    // 2. This access result is not cacheable and $other's access result is the
    // same. i.e. attempt to return a cacheable access result.

    Still trying to figure out the logic behind the current implementation... maybe if both access results say allowed and one would kill caching then optimize for a better cache hit ratio and use the maximum cache max-age value instead of the minimum (like in andIf())?

  • πŸ‡­πŸ‡ΊHungary mxr576 Hungary

    Even after πŸ› AccessResult::orIf() has meaningless checks Fixed was closed, the reported issue still exists and the merge is decided by the same (refactored) line.

    https://git.drupalcode.org/project/drupal/-/commit/1c814ad4277296875b9bd...

  • πŸ‡³πŸ‡±Netherlands kingdutch

    This is something that can be evaluated using a truth table (using Allowed for TRUE and Forbidden for FALSE).

    | Partial Result | A Allowed | A Forbidden |
    | B Allowed      | Allowed   | Allowed     |
    | B Forbidden    | Allowed   | Forbidden   |
    

    Based on these results we can see that if B is allowed (TRUE) then the result itself is independent of A. So if B is Allowed then we indeed don't need to look at A or A's cacheability. So with that in mind I would expect that if B is forever cacheable and B is allowed, then the result is also forever cacheable; because the result won't change if A changes.

    It's important that in case B is Forbidden (FALSE) we do cache based on A's cacheability because it entirely dictates the possible outcome.

    Because of the commutative properties of the "or" operation, I would expect this behaviour also if B (endlessly cacheable) was provided first and A (uncacheable) was provided in the orIf.

    ----------

    The above is how I believe, based on the described logic, how Drupal should work. So this would be contradicting your steps to reproduce (where the result should be "cacheable to infinity"). However, I don't quite understand Drupal's comments so I can't comment on whether the current implementation matches the above outline.

  • πŸ‡­πŸ‡ΊHungary mxr576 Hungary
  • Pipeline finished with Canceled
    4 months ago
    Total: 515s
    #196226
  • Pipeline finished with Canceled
    4 months ago
    Total: 259s
    #196237
  • Pipeline finished with Canceled
    4 months ago
    Total: 131s
    #196242
  • Pipeline finished with Failed
    4 months ago
    Total: 567s
    #196244
  • Status changed to Closed: works as designed 4 months ago
  • πŸ‡­πŸ‡ΊHungary mxr576 Hungary

    Okay, it took some time to realize that for sure, this works as designed.

Production build 0.71.5 2024