Problem/Motivation

I can't comprehend orIf/andIf. And think the interface could use a little love too in methods and a lot in documentation. Finally, there has been a bit of contention because AccessResult has these methods that are not on the interface.

Proposed resolution

  1. Rewrite the code and doxygen both for andIf() and orIf().
  2. Add an isNeutral() method.
  3. Make AccessResult immutable allowing the removal of all non-interface instance methods.
  4. Name every factory method X mirroring the isX interface methods: allowed, forbidden, neutral vs isAllowed, isForbidden, isNeutral.
  5. Document the whole shebang implementing a Kleene's weak three-valued logic. It would have saved me quite some time knowing that -- if you ever want to know what isNeutral AND isAllowed wants to be, just open Wikipedia at the truth table knowing that isNeutral maps to F and isAllowed maps to T (and you know that because it's documented!) and there you are. We spent half an hour yesterday on the phone with Wim trying to decipher this.

Remaining tasks

Review.

User interface changes

None.

API changes

  1. orIf()/andIf() now return new objects rather than modifying the first operand.
  2. Added AccessResultInterface::isNeutral().
  3. Renamed AccessResult::create() to AccessResult::neutral() — this does not change AccessResultInterface though!
  4. Removed the allow(), forbid(), resetAccess() instance methods on AccessResult — this does not change AccessResultInterface though!
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

chx’s picture

FileSize
6.84 KB

Edit: I was wrong.

chx’s picture

Title: The new orIf/andIf is confusing, non-commutative and probably insecure » [sechole] The new orIf/andIf is confusing, non-commutative and insecure on top
Priority: Normal » Critical
Issue summary: View changes
Status: Active » Needs work
FileSize
8.73 KB

This is better. Renames less and thus highlights the sechole. It also shows off the new API :)

chx’s picture

Assigned: Unassigned » Wim Leers

Now this is Wim's. I am done here :)

chx’s picture

Title: [sechole] The new orIf/andIf is confusing, non-commutative and insecure on top » The new orIf/andIf is very confusing
Priority: Critical » Normal
Issue summary: View changes
FileSize
9.97 KB
chx’s picture

Issue summary: View changes
chx’s picture

Title: The new orIf/andIf is very confusing » The new AccessResult API and implementation is very confusing
Issue summary: View changes
Status: Needs work » Needs review
FileSize
114.63 KB

Status: Needs review » Needs work

The last submitted patch, 6: 2340507_6.patch, failed testing.

chx’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
64.1 KB
chx’s picture

Issue summary: View changes
chx’s picture

Issue summary: View changes
dawehner’s picture

Issue summary: View changes

I really like that we do have now a proper foundation and no hand-wavy implementation any longer.

  1. +++ b/core/lib/Drupal/Core/Access/AccessResult.php
    @@ -392,67 +323,43 @@ public function cacheUntilEntityChanges(EntityInterface $entity) {
        */
       public function orIf(AccessResultInterface $other) {
    -    // If this AccessResult already is forbidden, then that already is the
    -    // conclusion. We can completely disregard $other.
    -    if ($this->isForbidden()) {
    -      return $this;
    +    if ($this->isForbidden() || $other->isForbidden()) {
    +      $result = static::forbidden();
    +    }
    +    elseif ($this->isAllowed() || $other->isAllowed()) {
    +      $result = static::allowed();
         }
    -    // Otherwise, we make this AccessResult forbidden if the other is, or
    -    // allowed if the other is, and we merge in the cacheability metadata if the
    -    // other access result also has cacheability metadata.
         else {
    -      if ($other->isForbidden()) {
    -        $this->forbid();
    -      }
    -      else if ($other->isAllowed()) {
    -        $this->allow();
    -      }
    -      $this->mergeCacheabilityMetadata($other);
    -      return $this;
    +      $result = static::neutral();
         }
    +    $result->mergeCacheabilityMetadata($this);
    +    if (!$this->isForbidden()) {
    +      $result->mergeCacheabilityMetadata($other);
    +    }
    +    return $result;
       }
     
       /**
        * {@inheritdoc}
        */
       public function andIf(AccessResultInterface $other) {
    -    // If this AccessResult already is forbidden or is merely not explicitly
    -    // allowed, then that already is the conclusion. We can completely disregard
    -    // $other.
    -    if ($this->isForbidden() || !$this->isAllowed()) {
    -      return $this;
    +    if ($this->isForbidden() || $other->isForbidden()) {
    +      $result = static::forbidden();
    +    }
    +    elseif ($this->isAllowed() && $other->isAllowed()) {
    +      $result = static::allowed();
         }
    -    // Otherwise, we make this AccessResult forbidden if the other is, or not
    -    // explicitly allowed if the other isn't, and we merge in the cacheability
    -    // metadata if the other access result also has cacheability metadata.
         else {
    -      if ($other->isForbidden()) {
    -        $this->forbid();
    -      }
    -      else if (!$other->isAllowed()) {
    -        $this->resetAccess();
    -      }
    -      $this->mergeCacheabilityMetadata($other);
    -      return $this;
    +      $result = static::neutral();
    ...
       }
    

    10 times better!

  2. +++ b/core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php
    @@ -76,7 +76,7 @@ public function access(EntityInterface $entity, $operation, $langcode = Language
    -    if (!$return->isAllowed() && !$return->isForbidden()) {
    +    if ($return->isNeutral()) {
    

    MUCH BETTER!

  3. +++ b/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php
    @@ -201,36 +137,69 @@ public function testAndIf() {
    -    $access = clone $allowed;
    -    $access->andIf($forbidden);
    +    $access = $allowed->andIf($forbidden);
    +    $this->assertFalse($access->isAllowed());
    +    $this->assertTrue($access->isForbidden());
    +    $this->assertDefaultCacheability($access);
    +
    +    // DENY && ALLOW == DENY
    +    $access = $no_opinion->andIf($allowed);
    +    $this->assertFalse($access->isAllowed());
    +    $this->assertFalse($access->isForbidden());
    +    $this->assertDefaultCacheability($access);
    +
    +    // DENY && DENY === DENY.
    +    $access = $no_opinion->andIf($no_opinion);
    +    $this->assertFalse($access->isAllowed());
    +    $this->assertFalse($access->isForbidden());
    +    $this->assertDefaultCacheability($access);
    +
    +    // DENY && KILL === KILL.
    +    $access = $no_opinion->andIf($forbidden);
    +    $this->assertFalse($access->isAllowed());
    +    $this->assertTrue($access->isForbidden());
    +    $this->assertDefaultCacheability($access);
    +
    +    // KILL && ALLOW = KILL
    +    $access = $forbidden->andif($allowed);
    +    $this->assertFalse($access->isAllowed());
    +    $this->assertTrue($access->isForbidden());
    +    $this->assertDefaultCacheability($access);
    +
    +    // KILL && DENY = KILL
    +    $access = $forbidden->andif($no_opinion);
    +    $this->assertFalse($access->isAllowed());
    +    $this->assertTrue($access->isForbidden());
    +    $this->assertDefaultCacheability($access);
    +
    +    // KILL && KILL = KILL
    +    $access = $forbidden->andif($forbidden);
         $this->assertFalse($access->isAllowed());
    

    Did we considered to update the docs here to talk about neutral? and $neutral?

chx’s picture

Issue summary: View changes

> Did we considered to update the docs here to talk about neutral? and $neutral?

Yes I put that suggestion in the summary while you were reading the patch :D but it got cross-posted. Restored.

Status: Needs review » Needs work

The last submitted patch, 8: 2340507_8.patch, failed testing.

effulgentsia’s picture

I only skimmed this so far, but I like what I see. I'll leave it to Wim and dawehner to review in-depth and RTBC though.

chx’s picture

Issue summary: View changes
chx’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
65.14 KB

This won't pass but it makes checkAll simpler and "rhyme" with checkAny.

Fun: AccessManagerTest passes just the same if you remove the $result->andIf() call from checkAll.

Status: Needs review » Needs work

The last submitted patch, 16: 2340507_15.patch, failed testing.

chx’s picture

Status: Needs work » Needs review
FileSize
65.98 KB

Closer...

Status: Needs review » Needs work

The last submitted patch, 18: 2340507_18.patch, failed testing.

chx’s picture

Status: Needs work » Needs review
FileSize
68.25 KB
4.26 KB

Even closer...

Status: Needs review » Needs work

The last submitted patch, 20: 2340507_20.patch, failed testing.

chx’s picture

FileSize
68.29 KB
1.19 KB
chx’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 22: 2340507_22.patch, failed testing.

aheimlich’s picture

+++ b/core/modules/contact/src/Access/ContactPageAccess.php
@@ -71,9 +71,10 @@ public function access(UserInterface $user, AccountInterface $account) {
+    if ($permission_access->allowed()) {

Shouldn't that be $permission_access->isAllowed()?

chx’s picture

Status: Needs work » Needs review
FileSize
69.42 KB
2.81 KB

Yes, thanks. Fixed that and more. I am not happy with the change in ContentTranslationOverviewAccess cos cacheability might not propagate but I am not even clear whether it should I need to talk to Wim about this caching stuff.

Even if someone else decides to post the next patch please keep this interdiff visible for the reasons above.

chx’s picture

Thanks aheimlich SO MUCH! I have missed that one and look, now we are green!

chx’s picture

Issue summary: View changes
Wim Leers’s picture

Title: The new AccessResult API and implementation is very confusing » The new AccessResult API and implementation is confusing
Issue summary: View changes
Issue tags: +DX (Developer Experience)
FileSize
100.34 KB
41.81 KB

Code review

Overall this patch looks excellent:

  1. public function isNeutral() {
    

    YAY! I thought this might be contentious, which is why I didn't add it previously. I like this much better though :)

  2. +++ b/core/lib/Drupal/Core/Access/AccessResultInterface.php
    @@ -36,43 +34,56 @@
    +   * This is a kill switch -- both orIf and andIf will result in isForbidden()
    +   * if either results are isForbidden().
    ...
        * - isForbidden() in either ⇒ isForbidden()
    -   * - isAllowed() in both ⇒ isAllowed()
    -   * - neither isForbidden() nor isAllowed() => !isAllowed() && !isForbidden()
    +   * - otherwise, if isAllowed() in both ⇒ isAllowed()
    +   * - otherwise, one of them is isNeutral()  ⇒ isNeutral()
    

    Good clarifications :)

  3. +++ b/core/modules/content_translation/src/Access/ContentTranslationOverviewAccess.php
    @@ -74,10 +74,10 @@ public function access(RouteMatchInterface $route_match, AccountInterface $accou
    -      return $access->allowIfHasPermission($account, $permission);
    +      return $access->orIf(AccessResult::allowedIfHasPermission($account, $permission));
    

    I'm not a big fan of these, but it allows for a smaller (and hence simpler) API, so I think that's more than worth it :)
    It also more clearly shows what's going on; it stresses the use of orIf() and andIf() more, so that's great.
    Taking that fact into consideration, I *am* a fan of this :)

  4. +++ b/core/modules/menu_link_content/src/MenuLinkContentAccessControlHandler.php
    @@ -55,23 +55,21 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
    -          $access = AccessResult::create()->cachePerRole()->cacheUntilEntityChanges($entity);
               // If there is a URL, this is an external link so always accessible.
    -          if ($entity->getUrl()) {
    -            return $access->allow();
    -          }
    -          else {
    +          $access = AccessResult::allowed()->cachePerRole()->cacheUntilEntityChanges($entity);
    +          if (!$entity->getUrl()) {
                 // We allow access, but only if the link is accessible as well.
                 $link_access = $this->accessManager->checkNamedRoute($entity->getRouteName(), $entity->getRouteParameters(), $account, TRUE);
    -            return $access->allow()->andIf($link_access);
    +            return $access->andIf($link_access);
               }
    +          return $access;
    

    Much clearer :)

  5. +++ b/core/modules/system/tests/modules/entity_test/entity_test.module
    @@ -337,7 +337,7 @@ function entity_test_entity_field_access($operation, FieldDefinitionInterface $f
    -    $grants[':default']->forbid()->cacheUntilEntityChanges($context['items']->getEntity());
    +    $grants[':default'] = AccessResult::forbidden()->cacheUntilEntityChanges($context['items']->getEntity());
    

    Problematic: cacheability metadata lost!

  6.     // andIf(); 1st doesn't implement CacheableInterface, 2nd does.
        $access = $access_without_cacheability->andIf(AccessResult::allowed());
        $this->assertTrue($access->isAllowed());
        $this->assertFalse($access->isForbidden());
        $this->assertFalse($access->isNeutral());
        $this->assertFalse(method_exists($access, 'isCacheable'));
    

    This test was not updated and is therefore now subtly broken. Fixed in this reroll.

In this reroll

From the IS:

  1. Write out the nine orIf tests.
  2. Add more to AccessManagerTest -- right now removing $result->andIf() doesn't fail it.
  3. Add more asserts for caching -- we now have every case covered, surely we can test that cache merging works as we wanted. That will also serve as documentation.
  4. I think we should remove the DENY word -- now it only exists on the test, mind you. The constant is gone.
  5. Review the changes to ContentTranslationOverviewAccess in #26 -- is the cache propagation right?
  1. Done.
  2. Reproduced, but as per our discussion in IRC: that's out of scope. AccessManager test coverage is apparently weak, but that's not caused by #2287071: Add cacheability metadata to access checks.
  3. Good call. In doing so, I discovered that your changes were wrong. But I also discovered that my original code was similarly, but differently flawed. It was "less wrong", but still wrong. This shows once again that you really can't have too much test coverage of the low-level/"fundamental" code. I have to say that the fact you noticed this was a formal system, called Kleene's weak three-valued logic (http://en.wikipedia.org/wiki/Many-valued_logic) really helped with this. The "contagiousness" aspect is very important, because it naturally also applies to cacheability metadata.
    I'm glad my patch (#2287071: Add cacheability metadata to access checks) triggered your critical eye, because I merely translated what already was there and made everything use a single consistent system. I think this explicit formalness really helps understand the access system much better. It also gives peace of mind, knowing that this isn't something ad-hoc, but something solid :)
    I present to you, all 72 different permutations, times two operators, for a grand total of 144 test cases.
  4. Agreed; done. That was s/DENY/NEUTRAL/. Also did s/KILL/FORBIDDEN/ and s/ALLOW/ALLOWED/.
  5. There's indeed a problem with the cache propagation there. Now fixed, by renaming mergeCacheabilityMetadata() to inheritCacheability(), making it public, and using it here. It will only be necessary in rare cases (the only other case in core is in point 5 of my dreditor review: entity_test_entity_field_access(), which is in a test module, and a similar example in entity.api.php). Having the cacheability inheriting code as a public instance method also is very consistent with the cleaned up AccessResult: all instance methods are related to cacheability now!

Besides that, I also did:

  1. Clean up docs.
  2. Add isNeutral() assertions wherever there are isAllowed() and isForbidden() assertions.

The "very" is a bit hyperbole — I've had several people thank me for "such a nice API".

Status: Needs review » Needs work

The last submitted patch, 29: 2340507-29.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
100.63 KB
4.74 KB

14 fails, 3 exceptions. 13 of those due to something very stupid: no return $this; in inheritCacheability() — fixed and added test coverage. Off to dinner and a green patch!

chx’s picture

Title: The new AccessResult API and implementation is confusing » Make the new AccessResult API and implementation even better
Wim Leers’s picture

#32: :)

dawehner’s picture

Great work! Especially the expanded test coverage!

From AccessManager.php:

  protected function checkAny(array $checks, ArgumentsResolverInterface $arguments_resolver) {
    // No opinion by default.
    $result = AccessResult::neutral();

    foreach ($checks as $service_id) {
      if (empty($this->checks[$service_id])) {
        $this->loadCheck($service_id);
      }
      $result = $result->orIf($this->performCheck($service_id, $arguments_resolver));
      // Stop as soon as the first forbidden check is encountered.
      if ($result->isForbidden()) {
        break;
      }
    }

    return $result;
  }

It is not obvious for me, whether you can actually break in cases of OR like that. Do you maybe have to take into account all of them, in order to have all cache information stored? Some line of documentation in case you don't would be cool, why is that possible?

  1. +++ b/core/lib/Drupal/Core/Access/AccessResult.php
    @@ -17,31 +17,7 @@
    +abstract class AccessResult implements AccessResultInterface, CacheableInterface {
    

    It would be great if we could clearly document that the access specific (non-cache) information of these access result are immutable.

  2. +++ b/core/lib/Drupal/Core/Access/AccessResult.php
    @@ -153,83 +131,38 @@ public static function forbiddenIf($condition) {
       /**
        * {@inheritdoc}
    +   *
    +   * @see \Drupal\Core\Access\AccessResultAllowed
        */
       public function isAllowed() {
    ...
    +    return FALSE;
       }
    ...
        *
    ...
    +   * \Drupal\Core\Access\AccessResultForbidden
        */
    ...
    +  public function isForbidden() {
    +    return FALSE;
       }
     
    

    Let's put an @see on both of them.

  3. +++ b/core/lib/Drupal/Core/Access/AccessResult.php
    @@ -392,76 +325,88 @@ public function cacheUntilEntityChanges(EntityInterface $entity) {
       public function orIf(AccessResultInterface $other) {
    ...
    +    // The other access result's cacheability metadata is merged if $merge_other
    

    Small detail, if you would use "The $other" it would be a little bit better to understand.

  4. +++ b/core/lib/Drupal/Core/Access/AccessResult.php
    @@ -392,76 +325,88 @@ public function cacheUntilEntityChanges(EntityInterface $entity) {
    +    // 1. The other access result is used.
    

    Just from reading this comment it seems not clear what means used here (either it is forbidden or allowed, right?)

  5. +++ b/core/lib/Drupal/Core/Access/AccessResult.php
    @@ -392,76 +325,88 @@ public function cacheUntilEntityChanges(EntityInterface $entity) {
         else {
    -      if ($other->isForbidden()) {
    -        $this->forbid();
    +      $result = static::neutral();
    +      if (!$this->isNeutral()) {
    +        $merge_other = TRUE;
           }
    -      else if (!$other->isAllowed()) {
    -        $this->resetAccess();
    +    }
    

    I don't understand why we don't have to merge cache info, if $this is neutral and $other is Allowed

  6. +++ b/core/lib/Drupal/Core/Access/AccessResult.php
    @@ -392,76 +325,88 @@ public function cacheUntilEntityChanges(EntityInterface $entity) {
    +    // Only when this access result already was isAllowed(), the second
    +    $result->inheritCacheability($this);
    

    Docs seems to stop in the middle of

  7. +++ b/core/lib/Drupal/Core/Access/AccessResultAllowed.php
    @@ -0,0 +1,19 @@
    +
    +class AccessResultAllowed extends AccessResult {
    +
    
    +++ b/core/lib/Drupal/Core/Access/AccessResultForbidden.php
    @@ -0,0 +1,19 @@
    +
    +class AccessResultForbidden extends AccessResult {
    +
    

    Nitpick: One-liner needed.

  8. +++ b/core/lib/Drupal/Core/Access/AccessResultInterface.php
    @@ -23,12 +23,10 @@
    - * @endcode
    + * Objects implementing this interface are using Kleene's weak three-valued
    + * logic with the isAllowed() state being TRUE, the isForbidden() state being
    + * the intermediate truth value and isNeutral() being FALSE. See
    + * http://en.wikipedia.org/wiki/Many-valued_logic for more.
      */
     interface AccessResultInterface {
    

    #2157541: Views sets access to ANY on routes - could result in information disclosure could use a 4th state which returns AccessResultAllowed for all cases (AND/OR) but I guess we need to find a different solution for that.

  9. +++ b/core/lib/Drupal/Core/Access/AccessResultInterface.php
    @@ -36,43 +34,56 @@
    +  /**
        * Combine this access result with another using OR.
        *
    ...
    +   * @return \Drupal\Core\Access\AccessResultInterface
        */
       public function orIf(AccessResultInterface $other);
    ...
       /**
    ...
        *
    -   * @return $this
    +   * @return \Drupal\Core\Access\AccessResultInterface
        */
       public function andIf(AccessResultInterface $other);
    

    You can use @return static here

  10. +++ b/core/lib/Drupal/Core/Access/CsrfAccessCheck.php
    @@ -49,17 +49,16 @@ public function __construct(CsrfTokenGenerator $csrf_token) {
    +    // Not cacheable because the CSRF token is highly dynamic.
    +    return $result->setCacheable(FALSE);
    

    Technical this is by session and URL, right? Do we have a cache context of a session already?

  11. +++ b/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php
    @@ -159,80 +120,101 @@ public function testAccessConditionallyAllowed() {
       public function testAndIf() {
    
    @@ -240,60 +222,81 @@ public function testAndIf() {
       public function testOrIf() {
    

    Did we considered to convert those to use a data provider as well? It lets you provide the available combinations in a better readable way.

  12. +++ b/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php
    @@ -159,80 +120,101 @@ public function testAccessConditionallyAllowed() {
         $unused_access_result_due_to_lazy_evaluation = $this->getMock('\Drupal\Core\Access\AccessResultInterface');
         $unused_access_result_due_to_lazy_evaluation->expects($this->never())
           ->method($this->anything());
     
    ...
    +    // FORBIDDEN && * === FORBIDDEN: lazy evaluation verification.
    +    $access = $forbidden->andIf($unused_access_result_due_to_lazy_evaluation);
    +    $this->assertFalse($access->isAllowed());
    

    Awesome!

  13. +++ b/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php
    @@ -494,39 +478,404 @@ public function testCacheabilityMerging() {
    +  public function testAndOrCacheabilityPropagationProvider() {
    

    Isn't this method executed as test, because of the prefix "test"? We use providerTestAndOr somewhere else ...

  14. +++ b/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php
    @@ -494,39 +478,404 @@ public function testCacheabilityMerging() {
    +    $allowed_un = new UncacheableTestAccessResult('ALLOWED');
    ...
    +    $neutral_un = new UncacheableTestAccessResult('NEUTRAL');
    ...
    +      [$allowed_un, 'OR', $allowed_ct, FALSE, NULL],
    +      [$allowed_un, 'OR', $allowed_cf, FALSE, NULL],
    +      [$allowed_un, 'OR', $allowed_un, FALSE, NULL],
    ...
    +      $result = $first->orIf($second);
    ...
    +      $result = $first->andIf($second);
    

    Mh, here we actually test the UncacheableTestAcessResult not AccessResult itself. I do understand why, but can we make this explicit?

  15. +++ b/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php
    @@ -494,39 +478,404 @@ public function testCacheabilityMerging() {
    +    else {
    +      throw new \LogicException('Invalid operator specified');
    +    }
    

    Do we still need this? Just curious.

  16. +++ b/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php
    @@ -494,39 +478,404 @@ public function testCacheabilityMerging() {
    +      $this->assertTrue($result instanceof CacheableInterface, 'Result is an instance of CacheableInterface.');
    ...
    +      $this->assertFalse($result instanceof CacheableInterface, 'Result is not an instance of CacheableInterface.');
    

    Note: There is assertInstanceOf

Wim Leers’s picture

FileSize
101.34 KB
7.3 KB

It is not obvious for me, whether you can actually break in cases of OR like that. Do you maybe have to take into account all of them, in order to have all cache information stored? Some line of documentation in case you don't would be cool, why is that possible?

OOohhh!!! Excellent point! That's true. That's also why I didn't do this in my original patch. AccessManager should just use AccessResultInterface::(andIf|orIf)(), which have comprehensive test coverage. If we try to do introduce such optimizations in AccessManager, we have to repeat that entire test coverage for AccessManager again. And as chx has shown already, AccessManager's test coverage is weak, so I've removed this optimization for now. We can restore it once #2341501: AccessManagerTest coverage is weak lands.

  1. Done.
  2. Hah, oops, fixed.
  3. Done.
  4. Improved.
  5. If we enter the third if-branch, the combined result is "neutral". We only merge $other's cacheability metadata if it actually affected the combined result. By definition, this branch means the combined result is "neutral". Therefore, we must only check if $this->isNeutral() is FALSE, because if and only if that's the case, we've used $other to come to the conclusion that the combined result is "neutral".
    Therefore: if $this is "neutral" and $other is "allowed", then we did not use $other to conclude that the combined result must be "neutral" and hence we must also not merge its cacheability metadata.
  6. Hehe :) Removed, was no longer necessary.
  7. Fixed, also for the two other subclasses.
  8. … yeah that's out of scope, sorry.
  9. Oh! I did not know that. Thanks. Fixed!
  10. No, we don't have a "session" cache context, but we could definitely add that. Let's do that in a follow-up? Filed #2341991: Add SessionCacheContext, use it in CsrfAccessCheck.
  11. This was already there, I just made it explicit in the comment now :)
  12. :)
  13. ROFL :D :D :D You're absolutely right! This was being executed as a test, but one with zero test cases. Amazing catch :)
  14. Good point, documented.
  15. It's not strictly necessary, but there's also no harm. Better be strict than sorry. :)
  16. Oh! I wanted to update the patch to use that, but the downside is that you then have to specify the interface as a string, which is less handy than this. So keeping this as is.
chx’s picture

FileSize
644 bytes
98.39 KB

Don't try to do shortcuts in checkAll either. It's not worth the bothering, it's a micro optimization and the effects on caching is hard to understand and test.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

We have some follows ups and the current progress makes things surprisingly better to understand!

Wim Leers’s picture

chx: oops, I forgot to check if there was a similar optimization there. Thanks!

Yay for progress and simplicity!

xjm’s picture

Category: Bug report » Task
Priority: Normal » Major
Issue tags: +beta target
xjm’s picture

Issue tags: +Pre-AMS beta sprint

Great work here; let's try to get this one in soon because BC breaks in this API are a big deal.

dawehner’s picture

alexpott’s picture

What about negation...

 * Objects implementing this interface are using Kleene's weak three-valued
 * logic with the isAllowed() state being TRUE, the isForbidden() state being
 * the intermediate truth value and isNeutral() being FALSE. See
 * http://en.wikipedia.org/wiki/Many-valued_logic for more.

This means the opposite of isForbidden is isForbidden??? Seems odd to me.

chx’s picture

Yes we talked about negation and decided there is no use case and so we won't break out brains over it. But yes, it DOES mean that, I am really sorry, but the API in HEAD already does this, I haven't implemented this, I just put the formalization label on it and made the implementation of andIf/orIf crystal clear.

Also, this behavior is the only that makes any sense. Consider that a) we want isForbidden to stick across operations b) we want X AND X to be X (same for OR) this means that the only thing we can have any discussion over is what the ops should result when fed an isNeutral and an isAllow but that's also quite easy: if you are ORing you want that to go to allow. As for isNeutral AND isAllow: 1) can't really be isForbidden that makes no sense 2) we only want the result to be isAllow if both sides are isAllow so isNeutral AND isAllow goes to isNeutral. This finishes the truth table.

Wim Leers’s picture

Basically, these are the truth tables.

Definitions
  1. A === isAllowed === "TRUE"
  2. N === isNeutral === "FALSE"
  3. F === isForbidden === "SUPER EXTREME MEGA FALSE" AKA "BLACK HOLE FALSE"
AND
  |A N F
--+-----
A |A N F
N |N N F
F |F F F
OR
  |A N F
--+-----
A |A A F
N |A N F
F |F F F

As you can tell: boolean logic for the A and the N, but with F being "contagious".


This indeed equals http://en.wikipedia.org/wiki/Many-valued_logic#Bochvar.27s_internal_thre.... If we use Wikipedia's notation, then:

  1. T (TRUE) === A == isAllowed()
  2. F (FALSE) === N == isNeutral()
  3. I (INDETERMINATE) === F === isForbidden()

Given this, This means the opposite of isForbidden is isForbidden??? Seems odd to me. suddenly perfectly matches what Kleene's week three-valued logic says: the negation of the indeterminate is the indeterminate, i.e. the negation of isForbidden() is isForbidden(). This is once again the "contagious" aspect. Wikipedia specifically calls this out also:

The intermediate truth value in Bochvar's "internal" logic can be described as "contagious" because it propagates in a formula regardless of the value of any other variable.[4]

chx’s picture

Here's perhaps a way to understand this: if you are negating then the logic values stay the same but the labels turn into a negated thing. So you have isForceAllow (I) forcing allowance. But negating never happens so this is a theoretical exercise.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Thanks for taking answering my question :)

Committed ff035fc and pushed to 8.0.x. Thanks!

We need to update https://www.drupal.org/node/2337377 and link it to this issue.

  • alexpott committed ff035fc on 8.0.x
    Issue #2340507 by chx, Wim Leers: Make the new AccessResult API and...
effulgentsia’s picture

+++ b/core/lib/Drupal/Core/Access/AccessResultInterface.php
@@ -23,12 +23,10 @@
+ * Objects implementing this interface are using Kleene's weak three-valued
+ * logic with the isAllowed() state being TRUE, the isForbidden() state being
+ * the intermediate truth value and isNeutral() being FALSE. See
+ * http://en.wikipedia.org/wiki/Many-valued_logic for more.

Related to #42, what purpose is this doc serving? Objects implementing this interface ARE NOT implementing Kleene logic. They are implementing a subset of that logic only: just conjunction and disjunction. I think we should either remove the doc entirely (since the andIf() and orIf() methods are themselves documented), or else tweak the doc to be more precise. Especially because calling isForbidden() "indeterminate" is confusing: it's not indeterminate, it's just contagious. Not opening an issue for that yet, but if someone wants to, go for it.

chx’s picture

the word is intermediate (being in the middle) not indeterminate. And yes it is odd but it gives you truth tables for free. If you remove that then I want to add truth tables to orIf / andIf ( I drawn them in ASCII before, it's not hard ).

Wim Leers’s picture

I also provided the truth tables in #44. I personally also think that would be clearer, the Wikipedia article is not so great.

chx’s picture

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

Wim Leers’s picture

Wim Leers’s picture