Problem/Motivation

The order off permissions in user.role.*.yml does not matter. The array_diff in Role::revokePermission does not normalize the array keys this leads to huge diffs during config export, because the array is treated as associative array in YAML.

Proposed resolution

Normalize the stored permissions via grantPermission/revokePermission.

Remaining tasks

User interface changes

API changes

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

webflo’s picture

k4v’s picture

Test added.

k4v’s picture

Status: Needs work » Needs review

The last submitted patch, 1: normalize-user-role-entity.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 2: enforce-order-of-permissions-2409129-2.patch, failed testing.

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 2: enforce-order-of-permissions-2409129-2.patch, failed testing.

k4v’s picture

alimac’s picture

Issue tags: -SprintWeekend +SprintWeekend2015

We should all try and use the same sprint tag. According to https://groups.drupal.org/node/447258 it should be SprintWeekend2015.

k4v’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 8: enforce-order-of-permissions-2409129-8.patch, failed testing.

webflo’s picture

Status: Needs work » Needs review
FileSize
2.39 KB

Status: Needs review » Needs work

The last submitted patch, 12: 2409129-12.patch, failed testing.

The last submitted patch, 12: 2409129-12.patch, failed testing.

webflo’s picture

Status: Needs work » Needs review
FileSize
4.16 KB
tstoeckler’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/user/src/Plugin/views/field/Permissions.php
    @@ -80,7 +80,6 @@ public function query() {
    -    $uids = array();
    
    @@ -105,11 +104,10 @@ public function preRender(&$values) {
    -      foreach ($uids as $uid) {
    -        if (isset($this->items[$uid])) {
    -          ksort($this->items[$uid]);
    -        }
    +      foreach ($this->items as &$permission) {
    +        ksort($permission);
           }
    +      $debug = 1;
    
    +++ b/core/modules/user/src/Tests/Views/HandlerFieldPermissionTest.php
    @@ -42,9 +42,9 @@ public function testFieldPermission() {
    +    $expected_permissions[$this->users[3]->id()][] = t('View user information');
         $expected_permissions[$this->users[3]->id()][] = t('Administer permissions');
         $expected_permissions[$this->users[3]->id()][] = t('Administer users');
    -    $expected_permissions[$this->users[3]->id()][] = t('View user information');
    

    Discussed this with @webflo in person:
    The previous code did not work because $uids was never populated. This now makes it work the way it seemingly was supposed to work all along. The question is whether that actually makes sense because the titles of the permissions are displayed but they are now sorted by machine name. But I guess having some sort of deterministic sorting - even if it is not exactly intuitive - is better than complete randomness. So I would say let's leave that in for now. Especially because of the test coverage, this will actually reduce the fragility of the testing system.

    Let's remove the $debug, though ;-)

  2. +++ b/core/modules/user/src/Tests/RoleEntityTest.php
    @@ -0,0 +1,48 @@
    +use Drupal\simpletest\KernelTestBase;
    ...
    +class RoleEntityTest extends KernelTestBase {
    

    Since we have to re-roll anyway, could be using \Drupal\KernelTests\KernelTestBase (TNG)

    For extra credit, then replace assertIdentical() -> assertEquals().

webflo’s picture

Status: Needs work » Needs review
FileSize
4.13 KB
tstoeckler’s picture

Status: Needs review » Reviewed & tested by the community

Awesome, thanks a lot!

KTBTNG++

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 17: 2409129-17.patch, failed testing.

claudiu.cristea’s picture

  1. +++ b/core/modules/user/src/Entity/Role.php
    @@ -131,6 +130,7 @@ public function grantPermission($permission) {
    +      $this->normalize();
    
    @@ -143,6 +143,7 @@ public function revokePermission($permission) {
    +    $this->normalize();
    
    @@ -186,4 +187,14 @@ public function preSave(EntityStorageInterface $storage) {
    +  /**
    +   * Normalizes the stored permissions.
    +   *
    +   * Permissions are always ordered alphabetically to avoid conflicts in the
    +   * exported configuration.
    +   */
    +  protected function normalize() {
    +    sort($this->permissions);
    +  }
    +
     }
    

    I don't understand why we need the ::normalize() wrapper to wrap a single PHP function. And the name is about normalizing while we are doing a sorting. Drop the method and use sort() directly in grantPermission() and revokePermission(). EDIT: revokePermission() doesn't need any sorting. Normally it should keep the same order as before revoking.

  2. +++ b/core/modules/user/src/Plugin/views/field/Permissions.php
    @@ -80,7 +80,6 @@ public function query() {
    -    $uids = array();
    
    @@ -105,10 +104,8 @@ public function preRender(&$values) {
    -      foreach ($uids as $uid) {
    -        if (isset($this->items[$uid])) {
    -          ksort($this->items[$uid]);
    -        }
    +      foreach ($this->items as &$permission) {
    +        ksort($permission);
    

    Why not adding the same sort() in Role::getPermission() and this change we can avoid.

  3. +++ b/core/modules/user/src/Tests/RoleEntityTest.php
    @@ -0,0 +1,48 @@
    +  /**
    +   * Modules to enable.
    +   *
    +   * @var array
    +   */
    

    {@inheritdoc}

  4. +++ b/core/modules/user/src/Tests/RoleEntityTest.php
    @@ -0,0 +1,48 @@
    +  public static $modules = [
    +    'system',
    +    'user'
    +  ];
    

    Inline?

  5. +++ b/core/modules/user/src/Tests/RoleEntityTest.php
    @@ -0,0 +1,48 @@
    +    $role = Role::create(array('id' => 'test_role'));
    +    $role->grantPermission('b');
    +    $role->grantPermission('a');
    +    $role->grantPermission('c');
    

    Let's use modern array representation with square brackets. Also this can be written in more compact way, by chaining the method calls:

    $role = Role::create(...)
      ->grantPermission(...)
      ->grantPermission(...)
      ->grantPermission(...);
    
claudiu.cristea’s picture

Extra.

+++ b/core/modules/user/src/Plugin/views/field/Permissions.php
index 0000000..39c6b16
--- /dev/null

--- /dev/null
+++ b/core/modules/user/src/Tests/RoleEntityTest.php

+++ b/core/modules/user/src/Tests/RoleEntityTest.php
@@ -0,0 +1,48 @@
+ * Contains \Drupal\user\Tests\RoleEntityTest.
...
+class RoleEntityTest extends KernelTestBase {

All roles tests are named following the pattern: UserRole*Test. Let's use the same convention.

kgoel’s picture

I am working on it.

kgoel’s picture

Status: Needs work » Needs review
FileSize
4.12 KB
1.37 KB

I don't understand why we need the ::normalize() wrapper to wrap a single PHP function. And the name is about normalizing while we are doing a sorting. Drop the method and use sort() directly in grantPermission() and revokePermission(). EDIT: revokePermission() doesn't need any sorting. Normally it should keep the same order as before revoking.

revokePermission() does need sorting because we would need to sort users in alphabetical order after the user is deleted and also sort removes array key.

Why not adding the same sort() in Role::getPermission() and this change we can avoid.

Since grantPermission() and revokePermission() are already sorted so you can't change the permissions

webflo’s picture

Status: Needs review » Reviewed & tested by the community

#23 addressed the concerns from #20. We have to sort on revokePermission because we want to normalize the array keys after we removed a permissions with array_diff.

webflo’s picture

Issue summary: View changes
claudiu.cristea’s picture

Status: Reviewed & tested by the community » Needs work

#20 is not addressed. Why keeping the useless ::normalize()?

Since grantPermission() and revokePermission() are already sorted so you can't change the permissions

Not enough. A piece of code can bypass the grantPermission() sort in this way:

$role->set('permissions') = $permissions;
We have to sort on revokePermission because we want to normalize the array keys after we removed a permissions with array_diff

array_diff() is not touching the order of first array in the arguments list.

  1. +++ b/core/modules/user/src/Tests/UserRoleEntityTest.php
    @@ -0,0 +1,44 @@
    +  /**
    +   * Modules to enable.
    +   * @var array
    +   */
    

    See #20.

  2. +++ b/core/modules/user/src/Tests/UserRoleEntityTest.php
    @@ -0,0 +1,44 @@
    +    $role = Role::create(['id' => 'test_role']);
    +    $role->grantPermission('b')
    

    See #20.

webflo’s picture

array_diff() is not touching the order of first array in the arguments list.

Right, but we have to reset the array keys. We could add array_value to make it more obvious, but sort resets the permissions already.

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

dawehner’s picture

Priority: Normal » Major

Not enough. A piece of code can bypass the grantPermission() sort in this way:

IMHO this is a weak point ... we have an API and module authors don't use it. This is a clear problem. When you use an API, you get benefits, when you don't, your life will be bad.

array_diff() is not touching the order of first array in the arguments list.

Well, this is not really the point here. This is about array keys, let me show an example: https://3v4l.org/Yf6Zq
As you see this is not longer a simple list with increasing array index, which results in a completely different YAML output.

@webflo and myself talked about that issue and we came to the conclusion on presave would be ideal, but we should skip the resorting, in case we actually sync configuration.

dawehner’s picture

webflo’s picture

Status: Needs work » Needs review
FileSize
3.3 KB

Alright, lets keep it simple.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

I love it. I've had merge conflicts because of this issue on every single issue.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 32: 2409129-32.patch, failed testing.

webflo’s picture

Status: Needs work » Needs review
FileSize
5.03 KB
claudiu.cristea’s picture

Status: Needs review » Reviewed & tested by the community

Great! RTBC if green.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

As far as I can see we should have an upgrade path here to fix existing roles. Nice test coverage btw.

+++ b/core/modules/user/tests/src/Kernel/Views/HandlerFieldPermissionTest.php
@@ -37,9 +37,9 @@ public function testFieldPermission() {
     // View user profiles comes first, because we sort by the permission
     // machine name.
+    $expected_permissions[$this->users[3]->id()][] = t('View user information');

Amusing that comment is now correct :)

webflo’s picture

Status: Needs work » Needs review
FileSize
5.69 KB
679 bytes

Here is the update path for existing roles.

claudiu.cristea’s picture

+++ b/core/modules/user/user.post_update.php
@@ -0,0 +1,21 @@
+ */
+use Drupal\user\Entity\Role;

Empty line before use

But still needs test for the upgrade path.

webflo’s picture

claudiu.cristea’s picture

Yes :)

  1. +++ b/core/modules/user/src/Tests/Update/UserUpdateOrderPermissions.php
    @@ -0,0 +1,41 @@
    +/**
    + * @file
    + * Contains \Drupal\user\Tests\Update\UserUpdateSortPermissions.
    + */
    

    Should be removed.

  2. +++ b/core/modules/user/src/Tests/Update/UserUpdateOrderPermissions.php
    @@ -0,0 +1,41 @@
    +      __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
    

    Use the latest: drupal-8-rc1.bare.standard.php.gz

  3. +++ b/core/modules/user/src/Tests/Update/UserUpdateOrderPermissions.php
    @@ -0,0 +1,41 @@
    +  public function testPermissionsOrder() {
    +    $this->runUpdates();
    +    array_map(function (Role $role) {
    +      $permissions = $role->getPermissions();
    +      sort($permissions);
    +      $this->assertIdentical($permissions, $role->getPermissions());
    +    }, Role::loadMultiple());
    +  }
    

    How do we know that permission were unsorted initially? I think we don't need to iterate on all roles. What we need is a single role with unsorted order. Then we assert, before running updates, that the list is not sorted and again after. If the core doesn't have such a case we need to add one via a fixture.

Status: Needs review » Needs work

The last submitted patch, 41: 2409129-41.patch, failed testing.

claudiu.cristea’s picture

+++ b/core/modules/user/src/Plugin/views/field/Permissions.php
--- /dev/null
+++ b/core/modules/user/src/Tests/Update/UserUpdateOrderPermissions.php

+++ b/core/modules/user/src/Tests/Update/UserUpdateOrderPermissions.php
@@ -0,0 +1,41 @@
+class UserUpdateSortPermissions extends UpdatePathTestBase {

The file and the class names don't match. Also the class name should end with *Test.

webflo’s picture

Status: Needs work » Needs review
FileSize
6.97 KB

authenticated contains permissions that are not in the correct order.

claudiu.cristea’s picture

Status: Needs review » Reviewed & tested by the community

Hope that bot agrees. Thank you!

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed and pushed f5be97c to 8.4.x and cf6312f to 8.3.x. Thanks!

Thanks for adding the update path with tests.

diff --git a/core/modules/user/user.post_update.php b/core/modules/user/user.post_update.php
index 3f19b0c..cdfa758 100644
--- a/core/modules/user/user.post_update.php
+++ b/core/modules/user/user.post_update.php
@@ -8,6 +8,11 @@
 use Drupal\user\Entity\Role;
 
 /**
+ * @addtogroup updates-8.3.x
+ * @{
+ */
+
+/**
  * Enforce order of role permissions.
  */
 function user_post_update_enforce_order_of_permissions() {
@@ -20,3 +25,7 @@ function user_post_update_enforce_order_of_permissions() {
   };
   array_map($entity_save, Role::loadMultiple());
 }
+
+/**
+ * @} End of "addtogroup updates-8.3.x".
+ */

Added some missing comments.

  • alexpott committed f5be97c on 8.4.x
    Issue #2409129 by webflo, k4v, kgoel, claudiu.cristea, dawehner,...

  • alexpott committed cf6312f on 8.3.x
    Issue #2409129 by webflo, k4v, kgoel, claudiu.cristea, dawehner,...

Status: Fixed » Closed (fixed)

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