Problem/Motivation

Discovered in #2938035-6: When PATCHing a field is disallowed, no reason is given for *why* this happens. This blocks that issue.

#2808233: REST 403 responses don't tell the user *why* access is not granted: requires deep Drupal understanding to figure out introduced \Drupal\Core\Access\AccessResultReasonInterface. It also introduced the inheriting of a the reason through ->orIf() and ->andIf() combinations.

I just noticed that if you do

$a = AccessResult::neutral('You do not have the FOO permission.');
$b = AccessResult::neutral();
$reason = $a->orIf($b)->getReason();

Then $reason === NULL… which means you've just lost the reason. This occurs in real-world circumstances. Which means, for example, on any site with hook_entity_field_access(), the reason for some field access operation being denied … will be erased. Resulting in a bad DX.

Proposed resolution

  1. Add tests.
  2. Fix.

Remaining tasks

None.

User interface changes

None.

API changes

None.

Data model changes

None.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers created an issue. See original summary.

Wim Leers’s picture

Status: Active » Needs review
FileSize
1.34 KB
Wim Leers’s picture

The last submitted patch, 2: 2938053-2.patch, failed testing. View results

effulgentsia’s picture

Priority: Minor » Major

If this is indirectly blocking #2930028: Comprehensive JSON API integration test coverage phase 1: for every entity type, individual resources only, which in turn blocks JsonApi from being added to core, then raising this to Major. If it ends up not needing to block any of that, then we can re-evaluate priority.

Wim Leers’s picture

#5: correct, this is indirectly blocking #2930028: Comprehensive JSON API integration test coverage phase 1: for every entity type, individual resources only, and will soon be directly blocking it.

e0ipso’s picture

This looks good to me. I'm hesitant to RTBC because I'd like to add an extra assertion $this->assertNull($neutral_reasonless->orIf($neutral_reasonless)->getReason());. But that's a very minor observation.

Wim Leers’s picture

FileSize
811 bytes
2.73 KB

Good call, done.

e0ipso’s picture

Status: Needs review » Reviewed & tested by the community

This is RTBC for me.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

I wanted to make sure we had test coverage for both $forbidden->orIf($neutral) and $neutral->orIf($forbidden); and it turns out we have an important typo in the test. We need to make the following change:

diff --git a/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php b/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php
index d9af59890f..7912f64237 100644
--- a/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php
+++ b/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php
@@ -337,7 +337,7 @@ public function testOrIf() {
     $this->assertDefaultCacheability($access);

     // FORBIDDEN || NEUTRAL === FORBIDDEN.
-    $access = $forbidden->orIf($allowed);
+    $access = $forbidden->orIf($neutral);
     $this->assertFalse($access->isAllowed());
     $this->assertTrue($access->isForbidden());
     $this->assertFalse($access->isNeutral());

Another thing I pondered was if we should test for reason precedence. Ie.

$neutral = AccessResult::neutral('neutral message');
$neutral_other = AccessResult::neutral('other neutral message');
$access = $neutral->orIf($neutral_other);
$this->assertEquals('neutral message', $access->getReason());
$access = $neutral_other->orIf($neutral);
$this->assertEquals('other neutral message', $access->getReason());

We don't have any other tests for this for the FORBIDDEN && FORBIDDEN where the same situation comes up. I also wonder if we should care about the information loss.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
1.53 KB
3.13 KB

Another thing I pondered was if we should test for reason precedence.

+1. Done! I should've done that in the original issue.

Wim Leers’s picture

FileSize
1.12 KB
3.86 KB

it turns out we have an important typo in the test

… and there was one more, even!

Fixed both.

Wim Leers’s picture

FileSize
2.94 KB
5.68 KB

And #10 made me realize that the problem this issue describes occurs not only for NEUTRAL || NEUTRAL, but also for FORBIDDEN || FORBIDDEN. A similar bug existed there (the reason could get lost). The fix was similar. In fact, now the ::orIf() logic has identical code for handling reason inheritance in both the "merge NEUTRAL || NEUTRAL" and the "merge FORBIDDEN || FORBIDDEN" cases, and both have complete test coverage now.

alexpott’s picture

Title: AccessResult::orIf() fails to retain the reason if both operands are neutral, but the first contains a reason and the second one does not » AccessResult::orIf() fails to retain the reason if both operands are neutral or forbidden, but the first contains a reason and the second one does not

Updating title accordingly.

Wim Leers’s picture

Oops, I wanted to do that, but obviously forgot!

Wim Leers’s picture

borisson_’s picture

Status: Needs review » Reviewed & tested by the community

This all looks very solid. @alexpott made some remarks in #10 and as far as I can tell, those have all been resolved.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed and pushed b6242dab05 to 8.6.x and f1d3e4a07c to 8.5.x. Thanks!

Crediting myself for a review that affected the shape of the patch.

  • alexpott committed b6242da on 8.6.x
    Issue #2938053 by Wim Leers, alexpott: AccessResult::orIf() fails to...

  • alexpott committed f1d3e4a on 8.5.x
    Issue #2938053 by Wim Leers, alexpott: AccessResult::orIf() fails to...
Wim Leers’s picture

This unblocked #2938035 — rebased patches at #2938035-28: When PATCHing a field is disallowed, no reason is given for *why* this happens ready!

It also unblocked #2954847, a similar issue for the contrib JSON API module!

Status: Fixed » Closed (fixed)

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