There are several places now in Drupal core, where you want some kind of logic which has N conditions in a single string
.Someone want to either select items which match to all this N conditions, or just one of them.

Back in the days taxonomy introduced the concept of "+" / ",". So if you specify "1,2,3" you want nodes to have all this terms,
but with "1+2+3" you want nodes to have just one of the terms.

That's used nowadays in #1956896: Add role based access checker and Views contextual filters in general.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

dawehner’s picture

Component: profile.module » routing system

I have no clue about a proper component, but profile is probably wrong ;)

Crell’s picture

As long as we document it and are consistent, I have no objection to + and , used in that way.

Now that the role checker is using it, it may make sense to do the same to the permission checker.

aspilicious’s picture

I agree with everything Crell says :)

effulgentsia’s picture

Well, it's a Drupalism, and aren't we trying to get rid of those? Wouldn't | and & or || and && be a bit more self documenting?

Note that for most route requirements (including _format and _method), Symfony evaluates the requirement as a regular expression, so you could see YAML like this:

requirements:
  _format:  html|rss

Where the "|" in a regular expression functions similarly to a logical OR.

However, this issue is related to a route requirement applied to an array ($account->roles) rather than a string, so I don't think regexes are a good fit for expressing that.

effulgentsia’s picture

Status: Active » Needs review
FileSize
3.56 KB

To avoid confusion with regex syntax, this patch implements the || and && approach, with single space required on each side.

dawehner’s picture

Good idea, though I fear this approach does not work for contextual filters in views.

It would be great to keep this trimming as it just improves DX. Any reason why not to keep it?

effulgentsia’s picture

Status: Needs review » Needs work

Ah, so we still need a string syntax that's URL friendly for contextual filters. I thought this issue was just about a syntax for YAML files. Hm, need to think about it some more then.

My initial thought on removing trimming is that Symfony doesn't do auto-trimming of other evaluations in _requirements, and I wanted to be consistent with that. No strong preference though.

Crell’s picture

I'd prefer to keep the trimming for the improved DX.

The reason for using + and , is exactly as dawehner said, that's what Views uses, and what Taxonomy has always used. Switching to &&/|| in the YAML file would be more sensible in the YAML, but inconsistent elsewhere. URLs have to stick with + and ,.

We either have a consistent Drupalism or a Drupalism in some places but not others. I don't have a strong opinion either way.

dawehner’s picture

Status: Needs work » Needs review
FileSize
4.61 KB

Yeah let's better be consistent for now.

Status: Needs review » Needs work

The last submitted patch, drupal-1986640-9.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
4.64 KB

Just removing the failures and some debugging.

Crell’s picture

This code doesn't support mixing and and or operations. Are we OK with that? (I am, because it's a crazy edge case to mix both here, but that should be a conscious decision.)

+++ b/core/modules/user/lib/Drupal/user/Access/PermissionAccessCheck.php
@@ -28,9 +33,22 @@ public function applies(Route $route) {
-    // @todo Replace user_access() with a correctly injected and session-using
-    //   alternative.

Let's keep this @todo.

aspilicious’s picture

If you need such an edge case you ca write your own system and plug it in :)

dawehner’s picture

FileSize
713 bytes
4.75 KB

This code doesn't support mixing and and or operations. Are we OK with that? (I am, because it's a crazy edge case to mix both here, but that should be a conscious decision.)

We can still add that later, if needed, but yeah I don't see many usecases for that.

Crell’s picture

+++ b/core/modules/user/lib/Drupal/user/Access/PermissionAccessCheck.php
@@ -20,7 +20,12 @@ class PermissionAccessCheck implements AccessCheckInterface {
-    return array_key_exists('_permission', $route->getRequirements());
+    foreach (array_keys($route->getRequirements()) as $key) {
+      if (strpos($key, '_permission') === 0) {
+        return TRUE;
+      }
+    }
+    return FALSE;
   }

I'm not sure I see the purpose for this change.

+++ b/core/modules/user/lib/Drupal/user/Access/PermissionAccessCheck.php
@@ -28,9 +33,24 @@ public function applies(Route $route) {
+    $split = explode(',', $permission);

We need a comment or docblock explaining that ,-separated means "any of" and +-separated means "all of". In the docblock of this method is probably sufficient.

dawehner’s picture

FileSize
1.18 KB
4.82 KB

Let's use this way to allow subplugins to be registered (so you can easily provide a access checker like the one for "access all views").

Status: Needs review » Needs work
Issue tags: -VDC

The last submitted patch, vdc-1986640-16.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
Issue tags: +VDC

#16: vdc-1986640-16.patch queued for re-testing.

damiankloip’s picture

+++ b/core/modules/user/lib/Drupal/user/Access/PermissionAccessCheck.phpundefined
@@ -17,20 +17,40 @@
+   * The requirement used as key.

Maybe 'The requirement key to be used' instead?

+++ b/core/modules/user/lib/Drupal/user/Access/PermissionAccessCheck.phpundefined
@@ -17,20 +17,40 @@
+    $split = explode(',', $permission);
...
+      $split = explode('+', $permission);

Maybe a couple of one liners around here for OR and AND logic.

+++ b/core/modules/user/lib/Drupal/user/Access/PermissionAccessCheck.phpundefined
@@ -17,20 +17,40 @@
+      return $access ? static::ALLOW : static::DENY;

Is it almost worth having a helper for this? not sure.

+++ b/core/modules/user/tests/Drupal/user/Tests/Access/PermissionAccessCheckTest.phpundefined
@@ -0,0 +1,98 @@
+    $request = new Request(array());
...
+    $access_check = new PermissionAccessCheck();

Could we just move these into a setUp() method? or does the use of data providers do a setUp/teadDown between each iteration?

+++ b/core/modules/user/tests/Drupal/user/Tests/Access/PermissionAccessCheckTest.phpundefined
@@ -0,0 +1,98 @@
+  if (!function_exists('user_access')) {

Just out of interest, is the an issue to do something with this? user access is going to, and is already, being used in OO code. Not a part of this issue, just to be clear.

dawehner’s picture

FileSize
3.71 KB
5.49 KB

Is it almost worth having a helper for this? not sure.

Putting this into another method would make it potentially harder to read.

Crell’s picture

Status: Needs review » Reviewed & tested by the community

Actually, now that global $user is a UserSession object, and is available via the request, we could probably do this instead:

$request->attributes->get('account')->hasPermission($permission);

... Except, crap, no, because there's only a getRoles() method, not a hasPermission() method. So that has to wait for #1966334: Convert user_access to User::hasPermission(), which adds that method. *sigh*

That's an internal followup, so can happen later. No sense blocking these patches on each other.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

So the thing is there is nothing to stop us from declaring permissions like so...

/**
 * Implements hook_permission().
 */
function action_permission() {
  return array(
    'administer actions, events, and parties' => array(
      'title' => t('Administer actions, events and parties'),
    ),
  );
}

We also need to consider the upgrade path here...

Crell’s picture

A change notice that permission names cannot have those characters and (as suggested in meatspace by xjm) just an exception if you try to declare a permission with non-alphanumeric-plus-space characters should suffice here.

Crell’s picture

Title: Discuss how to conjunct options to support AND/OR » Support AND/OR conjunctions for permission checks

For Alex...

dawehner’s picture

One problem I do see with that is that people started to use "." in the permission machine name. (Not sure which patch this was, though).

Crell’s picture

OK, alphanumeric plus period? :-) Is there a reason we can't just blacklist , and + (and maybe others, maybe not) to resolve this issue?

It probably affects roles too at the moment, and we already have conjunction support for those...

dawehner’s picture

I am fine with blacklisting "," and "+" though the question is how to enforce that properly. For roles we can add something in the UI, but for permissions we might check them somewhere and throw an error?

Crell’s picture

For route rebuild, we've taken the approach of "if your route has invalid properties in it, watchdog it and skip that entry".

A similar approach for permission rebuild (which then gets cached anyway) seems reasonable.

dawehner’s picture

Status: Needs work » Needs review
FileSize
4.71 KB
  • Ensured that you can't use "," nor "+" in the roles UI
  • Rerolled the patch.

I am not really sure what a proper place for checking the permissions would be. The current access checker is written in a way that it can't execute runtime code for every route building.

Crell’s picture

Well, I expected that all calls to get permission data went through some common front-end utility that we could modify. It looks like that's not the case, however, as there are module_invoke_all('permission') and $moduleHandler->getImplementations('permission') and $moduleHandler->invokeAll('permission') calls scattered all over the place. :-( That's a separate thing to fix. Drat.

dawehner’s picture

#29: permission-1986640-29.patch queued for re-testing.

damiankloip’s picture

  1. +++ b/core/modules/user/lib/Drupal/user/Access/PermissionAccessCheck.php
    @@ -17,20 +17,42 @@
    +   * The requirement key to be used.
    

    @var ...

  2. +++ b/core/modules/user/lib/Drupal/user/Access/PermissionAccessCheck.php
    @@ -17,20 +17,42 @@
    +   */
    +  protected $requirementKey = '_permission';
    ...
    +    return $this->requirementKey;
    

    Also, do we actually need to use a property for this? vs just returning the '_permission' string. I guess currently we kind of mix both ways.

  3. +++ b/core/modules/user/lib/Drupal/user/Access/PermissionAccessCheck.php
    @@ -17,20 +17,42 @@
    +    $permission = $route->getRequirement($this->requirementKey);
    +    // Separate for matching ANY checks.
    +    $split = explode(',', $permission);
    +    if (count($split) > 1) {
    +      $access = FALSE;
    +      foreach ($split as $permission) {
    +        // @todo Replace user_access() with a correctly injected and
    +        // session-using alternative.
    +        $access |= user_access($permission);
    +      }
    +      return $access ? static::ALLOW : static::DENY;
    +    }
    +    else {
    +      // Separate for matching ALL checks.
    +      $split = explode('+', $permission);
    +      $access = TRUE;
    +      foreach ($split as $permission) {
    +        $access &= user_access($permission);
    +      }
    +      return $access ? static::ALLOW : static::DENY;
    +    }
    

    So, we are using this pattern in a few places now (or atleast will be). I'm thinking it would be cool if we could abstract this out but not sure how possible that is right now as we'd need some nice way to invoke 'things'

  4. +++ b/core/modules/user/tests/Drupal/user/Tests/Access/PermissionAccessCheckTest.php
    @@ -0,0 +1,100 @@
    +  public function providerTestApplies() {
    

    Same as other comment above, do we need this much from our appliesTo() method? If we keep this, these methods should be testAppliesTo. actually where is the testApplies() test method?!

pingers’s picture

+++ b/core/modules/user/lib/Drupal/user/Access/PermissionAccessCheck.php
@@ -17,20 +17,42 @@
+      foreach ($split as $permission) {
+        // @todo Replace user_access() with a correctly injected and
+        // session-using alternative.
+        $access |= user_access($permission);
+      }

The OR condition loop can return as soon as TRUE condition is met. I'll concede it's a micro optimization, but if this is going to get broken out and re-used... efficiency is good.

dawehner’s picture

Thank you for the reviews!

Also, do we actually need to use a property for this? vs just returning the '_permission' string. I guess currently we kind of mix both ways.

I would love to be able to reuse the code for the views "access all views" checker, which has to be different as you can also configure access via the views UI.

So, we are using this pattern in a few places now (or atleast will be). I'm thinking it would be cool if we could abstract this out but not sure how possible that is right now as we'd need some nice way to invoke 'things'

Do you have an idea without over-abstracting all of it?

The OR condition loop can return as soon as TRUE condition is met. I'll concede it's a micro optimization, but if this is going to get broken out and re-used... efficiency is good.

Changed that.

The last submitted patch, permission-1986640-34.patch, failed testing.

Crell’s picture

Issue summary: View changes

I'm pretty sure this won't apply anymore, but after a year it would be good to get back to. I still think permissions should parallel roles in their syntax.

dawehner’s picture

Status: Needs work » Needs review
FileSize
3.77 KB

OOOH

tim.plunkett’s picture

+++ b/core/modules/user/user.services.yml
@@ -1,6 +1,7 @@
     class: Drupal\user\Access\PermissionAccessCheck
+    arguments: ['@current_user']
     tags:

This isn't needed, right? Otherwise this looks great.

dawehner’s picture

Good catch, yeah this was it in the old times. Rerolled against the access context issue.

Crell’s picture

Status: Needs review » Reviewed & tested by the community

Assuming bot OKs it.

chx’s picture

Note we currently use human names for machine names ie. ->hasPermission('access content') instead of ->hasPermission('access_content') and so it's not entirely impossible to encounter a plus or comma in there; however the chances of there being an actual permission on either side is next to nothing so I believe we can go ahead with this, but we should put such a rename on the radar and discuss whether it's doable in the 8.x at all. Otherwise I would very strongly suggest showing a warning somewhere when working with permissions if there are permissions containing such.

chx’s picture

What has happened here? I am not supposed to be able to delete files made by others.

dawehner’s picture

Expanded to put the logic onto the AccessResult.

Maybe someone can point me to a place where multiple hasPermission calls exists in the context of AccessResults.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/lib/Drupal/Core/Access/AccessResult.php
    @@ -159,6 +159,23 @@ public static function allowedIfHasPermission(AccountInterface $account, $permis
    +    return static::create()->allowIfHasPermissions($account, $permissions, $conjunction);
    
    @@ -407,6 +424,46 @@ public function allowIfHasPermission(AccountInterface $account, $permission) {
    +  public function allowIfHasPermissions(AccountInterface $account, $permissions, $conjunction = 'AND') {
    

    Can typehint this with array.

  2. +++ b/core/lib/Drupal/Core/Access/AccessResult.php
    @@ -407,6 +424,46 @@ public function allowIfHasPermission(AccountInterface $account, $permission) {
    +    if ($conjunction == 'AND' && $permissions) {
    

    lets !empty($permissions)

dawehner’s picture

Status: Needs work » Needs review
FileSize
10.62 KB
4.41 KB

Sure, found another instance.

Wim Leers’s picture

Status: Needs review » Needs work
Issue tags: +DX (Developer Experience), +Needs change record

Glad to see this issue was made simpler by #2287071: Add cacheability metadata to access checks :)

  1. +++ b/core/lib/Drupal/Core/Access/AccessResult.php
    @@ -159,6 +159,23 @@ public static function allowedIfHasPermission(AccountInterface $account, $permis
    +   *   The account for which to check a permission.
    
    @@ -407,6 +424,46 @@ public function allowIfHasPermission(AccountInterface $account, $permission) {
    +   *   The account for which to check a permission.
    

    s/a permission/permissions/

  2. +++ b/core/lib/Drupal/Core/Access/AccessResult.php
    @@ -159,6 +159,23 @@ public static function allowedIfHasPermission(AccountInterface $account, $permis
    +   *   The permissions to check for.
    
    @@ -407,6 +424,46 @@ public function allowIfHasPermission(AccountInterface $account, $permission) {
    +   *   The permissions to check for.
    

    s/for//?

  3. +++ b/core/modules/shortcut/src/ShortcutSetAccessControlHandler.php
    @@ -11,6 +11,7 @@
    +use Drupal\node\Plugin\views\filter\Access;
    

    This is accidental?

  4. +++ b/core/modules/user/src/Access/PermissionAccessCheck.php
    @@ -30,6 +30,15 @@ class PermissionAccessCheck implements AccessInterface {
    +    // Separate for matching ANY checks.
    +    $split = explode(',', $permission);
    +    if (count($split) > 1) {
    +      return AccessResult::allowedIfHasPermissions($account, $split, 'OR');
    +    }
    +    else {
    +      $split = explode('+', $permission);
    +      return AccessResult::allowedIfHasPermissions($account, $split, 'AND');
    +    }
    

    A bit more documentation would be welcome here I think?

    This also needs a change record.

    Also, s/$split/$permissions/?

  5. +++ b/core/modules/user/tests/src/Unit/PermissionAccessCheckTest.php
    @@ -0,0 +1,75 @@
    + * @group Routing
    

    Please also add @group Access.

  6. +++ b/core/modules/user/tests/src/Unit/PermissionAccessCheckTest.php
    @@ -0,0 +1,75 @@
    +      [[], AccessResult::allowedIf(FALSE)->cachePerRole()],
    +      [['_permission' => 'allowed'], AccessResult::allowedIf(TRUE)->cachePerRole()],
    +      [['_permission' => 'denied'], AccessResult::allowedIf(FALSE)->cachePerRole()],
    +      [['_permission' => 'allowed,denied'], AccessResult::allowedIf(TRUE)->cachePerRole()],
    +      [['_permission' => 'allowed,denied,other'], AccessResult::allowedIf(TRUE)->cachePerRole()],
    +      [['_permission' => 'allowed+denied'], AccessResult::allowedIf(FALSE)->cachePerRole()],
    

    Each case is repeated 3 times. Put it in variables, reuse those? Much more legible.

dawehner’s picture

Status: Needs work » Needs review
FileSize
10.28 KB
4.22 KB

https://www.drupal.org/node/2341759

Thank you for your review

Wim Leers’s picture

Issue tags: -Needs change record
FileSize
10.79 KB
1.61 KB

I've corrected the change record (it said _permissions rather than _permission).

Fixed two nitpicks.

I'd love to see at least one *.routing.yml file in Drupal core use this. Don't we have a single use case for this? I think we might've solved it using an access check, which makes it a bit difficult to find?

Crell’s picture

Drupal\views\ViewsAccessCheck maybe?

SwitchShortcutSet has a custom access callback that just calls out to a function. We can probably refactor that to eliminate the function anyway. The function has several permission checks but may be more complex than this can handle. Worth checking.

Those are the only promising examples I could find off hand.

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community

I don't think neither is viable. I also searched and couldn't find any. I guess this will have to do then.

Back to RTBC.

dawehner’s picture

Drupal\views\ViewsAccessCheck maybe?

I am convinced that you don't understand the problem space, but yeah let's please discuss this in the other issue.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Needs a reroll for the changes in AccessResult due to #2340507: Make the new AccessResult API and implementation even better

alexpott’s picture

Status: Fixed » Needs work

Hmmm wrong status :)

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
9.59 KB
2.38 KB

Heh :)

Rerolled!

Wim Leers’s picture

FileSize
11.85 KB

#54 lost the new unit test hunk. #54 interdiff applies to this patch.

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community

Back to RTBC per #50 — this is only a reroll to chase HEAD.

xjm’s picture

Per @dawehener, this issue blocks a proper solution in #2157541: Views sets access to ANY on routes - could result in information disclosure, so bumping to critical.

olli’s picture

  1. +++ b/core/modules/user/src/Access/PermissionAccessCheck.php
    @@ -31,6 +31,15 @@ class PermissionAccessCheck implements AccessInterface {
    +    // Allow to conjunct the permissions with OR (',') or AND ('+').
    

    Shouldn't this be + for OR and , for AND? Filed #2346105: Use 1+2+3 for OR, 1,2,3 for AND.

  2. +++ b/core/modules/user/tests/src/Unit/PermissionAccessCheckTest.php
    @@ -0,0 +1,78 @@
    + * @group AccessF
    

    AccessF..?

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

Needs work for #58 - I've committed #2346105: Use 1+2+3 for OR, 1,2,3 for AND
So

+    // Allow to conjunct the permissions with OR (',') or AND ('+').
+    $split = explode(',', $permission);
+    if (count($split) > 1) {
+      return AccessResult::allowedIfHasPermissions($account, $split, 'OR');
+    }
+    else {
+      $split = explode('+', $permission);
+      return AccessResult::allowedIfHasPermissions($account, $split, 'AND');
+    }

Needs fixing.

dawehner’s picture

Status: Needs work » Needs review

Let's fix it.

dawehner’s picture

Let's fix it.

Crell’s picture

Status: Needs review » Reviewed & tested by the community

Unless bot objects.

alexpott’s picture

Issue summary: View changes

Brought issue summary inline with #2346105: Use 1+2+3 for OR, 1,2,3 for AND

webchick’s picture

Status: Reviewed & tested by the community » Fixed

Committed and pushed to 8.x. Thanks!

  • webchick committed 8a15028 on 8.0.x
    Issue #1986640 by Wim Leers, chx, dawehner, effulgentsia: Support AND/OR...

Status: Fixed » Closed (fixed)

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

quietone’s picture

Published the CR