Problem definition

Currently, group roles are either assigned, special (anon/outsider/member) or synchronized to a global role provided you're not a member.

The drawbacks of these last "advanced outsider permissions" are plenty:

  • You lose them once you join a group
  • A role is created for every global role on every group type (yuck)
  • You have to assign permissions to a role that might be identical to a group role you already set up

Proposed alternative

We allow ALL group roles to indicate whether they belong to a Drupal role and if they apply to either outsiders, insiders or an individual member. A global role can be added to a group twice, once for outsider, once for insider. Then we convert the Anonymous, Outsider and Member group roles to these new versions.

Anyone with a synchronized role will automatically inherit the corresponding group role, like they do now, but they will now have the chance to also gain these permissions when they are a member of the group by inheriting a role from the insider scope. We will only allow one pair of scope/global_role per group type so you cannot set up the Anonymous group role twice.

Permission implications

The above will require a rewrite of the permission calculators and how they are cached. For example: outsider audience roles will now vary per user.roles cache context. The benefit is that the calculators are now agnostic of Group's internal kitchen and can be split off into its own module space.

The new scopes allow us to get rid of the huge IN conditions that list all of your membership IDs and instead write a few joins. After this rewrite, we know that all outsider audience roles will be granted based on your Drupal roles, so we can simply join the group_content data table and do an WHERE IS NULL to find all of the rows that state you're not a member. For that to work, we need to add 2 fields to a group_content data table: plugin and group_type. This is a denormalization that needs to happen because config entities are not stored in tables we can easily join.

CommentFileSizeAuthor
#16 break-all-the-things.diff197.65 KBkristiaanvandeneynde

Issue fork group-3204083

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

kristiaanvandeneynde created an issue. See original summary.

soul88’s picture

Version: 8.x-1.x-dev » 2.0.x-dev
kristiaanvandeneynde’s picture

Issue summary: View changes

Updating the IS to provide way more details.

kristiaanvandeneynde’s picture

A key difference is how we will join said table to figure out which groups you have outsider permissions to:

MySQL [drupal]> SELECT g.id, g.type, gc.entity_id FROM groups g LEFT JOIN group_content_field_data gc ON g.id = gc.gid AND gc.type LIKE '%-group_membership' AND entity_id = 3;
+----+---------+-----------+
| id | type    | entity_id |
+----+---------+-----------+
|  2 | bar     |         3 |
|  3 | default |      NULL |
|  4 | foo     |      NULL |
+----+---------+-----------+
3 rows in set (0.00 sec)

MySQL [drupal]> SELECT g.id, g.type, gc.entity_id FROM groups g LEFT JOIN group_content_field_data gc ON g.id = gc.gid AND gc.type LIKE '%-group_membership' WHERE entity_id = 3;
+----+------+-----------+
| id | type | entity_id |
+----+------+-----------+
|  2 | bar  |         3 |
+----+------+-----------+

As you can see, the former will join the groups table with all of the group memberships for user 3 or NULL if there is none. The latter will join ALL of the group memberships for all groups and then filter those owned by user 3. We definitely need the former.

This is because we want to be able to run a condition WHERE entity_id IS NULL so that we can find all of the groups the user isn't part of but are of the selected group types we know the user would have outsider access for. This should be a hell of a lot faster than an IN(thousands of memberships) condition on sites where people can join a large amount of groups.

kristiaanvandeneynde’s picture

kristiaanvandeneynde’s picture

As far as the roles are concerned, I've given it some more thought and I think we can do without special-casing anonymous. If we mark the role as non-member, we can still cache it using group.roles, BUT people can add extra calculators to it to perhaps give anonymous access to some specific groups (e.g.: free membership for one month to select groups).

The idea would be to have all roles flag as non-member or member and then add them to the calculated permissions as a "group type scoped" set of permissions. With the reworked table structure, we can then easily join on all of the group types that would give access and where the user is not a member of.

  1. Anonymous role would be: scope non-member, audience anonymous
  2. Outsider (Authenticated) role would be: scope non-member, audience outsider
  3. Any other global role that requires no membership: scope non-member, audience outsider
  4. Member role would be: scope group, audience member
  5. Any other global role that requires membership: scope group, audience member

You can clearly see why we can roll 3 into 2 and 5 into 4 here: They have become the same thing :) We can still optimize the anonymous permissions also because we know they can never be a member, so all the query has to check for is group types, we don't even need to join the group_content_field_data table for those.

kristiaanvandeneynde’s picture

Also, you should now be allowed to edit and delete the Member and Outsider role as you can always restore them. Probably the Anonymous role too, but I need to put some more thought into that.

kristiaanvandeneynde’s picture

Following up on #4, we can simplify the access query to the one shown there and then adding on the following:

WHERE
   # Outsider access for entire group type.
  (type IN types_with_global_outsider_access AND entity_id IS NULL)
OR
   # Insider access for entire group type
  (type IN types_with_global_insider_access AND entity_id IS NOT NULL)
OR
   # Specific (non-global) member roles access.
  id IN ids_with_specific_member_access

Where we would make sure that the list of IDs is not populated with any ID that is of a type allowed by types_with_global_insider_access. That should keep the list of IDs smaller.

jozzy_a’s picture

@kristiaan is this ticket going to bring in the functionality provided by the Group Outsider In module?

kristiaanvandeneynde’s picture

@jozzy_a judging by that module's description: Yup.

brad.bulger’s picture

Would it be feasible to extend this to cover roles in other groups? The subgroup modules have something like that, I know. In our case, neither group is really a subgroup of the other, they're just related.

kristiaanvandeneynde’s picture

This system would automatically work for Subgroup as it merely introduces one more calculator. It already does so in the current version and will continue to do so in the next.

brad.bulger’s picture

Sorry, I worded that unclearly. What I meant is, just as a person has access like Group Role X if they have Drupal Role Y, would it be feasible to grant them access like Group Role X if they are members of OtherGroup Z?

But I've been rereading and I may have a more basic misunderstanding. "A global role can be added to a group twice, once for every audience." - would that mean that you would create two group roles for a given Drupal role, like "Drupal Role Y for Non-Members" and "Drupal Role Y for Members"? or that you have two connections/rules that apply to one Group Role X and one Drupal Role Y?

kristiaanvandeneynde’s picture

Drupal Role Y for Non-Members" and "Drupal Role Y for Members

That is correct. It is necessary for the use case where you want the old behavior or you want a global user to have different permissions based on whether they're in or out of the group.

would it be feasible to grant them access like Group Role X if they are members of OtherGroup Z

That is how Subgroup works, yeah.

brad.bulger’s picture

OK, so if it is left to the subgroup modules, and the two groups are not hierarchically related, I need to use ggroup ("Subgroup(Graph)") to set up that access connection. The Group module itself will only deal with Drupal sitewide roles. I wanted to understand that better. Thanks.

kristiaanvandeneynde’s picture

Assigned: Unassigned » kristiaanvandeneynde
Status: Active » Needs review
StatusFileSize
new197.65 KB

This should break a bunch of tests, especially those that still use now removed constants, those that rely on default group roles being created, etc. These are good things as we needed to refactor those tests anyway. But let's see just how much we can break :D

Status: Needs review » Needs work

The last submitted patch, 16: break-all-the-things.diff, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

kristiaanvandeneynde’s picture

Error: Call to a member function grantPermission() on null is because the roles no longer get automatically created, which is a good thing.

Fixed a bunch of unit tests already. Going to use a merge request branch from this point forward.

kristiaanvandeneynde’s picture

Title: Rework "Advanced outsider permissions" into synchronized roles » 3204083-roles_rework
Status: Needs work » Needs review

So all that's left is to fix the field_storage going missing in Views tests, fix the EntityOperationsTest and move the query tests to new places while refactoring them. And then handle the todos, perhaps in follow-ups.

kristiaanvandeneynde’s picture

Title: 3204083-roles_rework » Rework "Advanced outsider permissions" into synchronized roles
Status: Needs review » Needs work
kristiaanvandeneynde’s picture

This patch is getting way too huge, I would normally reject that right away, but screw it. No-one's reviewing this beast and I'll make sure to write all the change records before this is marked fixed. Anyways: Introduced a base Views test class and fixed some issues.

kristiaanvandeneynde’s picture

I removed the chain permission calculator test and moved the code to specific tests per calculator. We might want to unit test the chain calculator, though, with dummy calculators.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review

Okay so I significantly updated the code for group and group content query alters and their tests. We should definitely have a follow-up issue where we look at EntityQueryAlter and also upgrade it accordingly.

If the tests go green (which they should), then the only thing that remains is to create smaller follow-up issues for all the todos added in this patch and to write a bunch of change records for the changes introduced here.

kristiaanvandeneynde’s picture

Issue summary: View changes

Updated title and IS to better reflect what happened here.

kristiaanvandeneynde’s picture

Title: Rework "Advanced outsider permissions" into synchronized roles » Rework group roles into a scoped system and adjust permission calculation
kristiaanvandeneynde’s picture

Things that need a change record:

Group role:

  • audience, internal and permissions_ui properties removed
  • scope and global_role properties added
  • advanced outsider removed
  • special roles removed, now an option on the group type form, methods on GT gone
  • synchronizer service removed

Calculators:

  • Heavily simplified into two methods
  • Put into their own module (create follow-up issue)
  • Have a base class now

Query access:

  • No more Entity API usage
  • All custom query handlers

Various:

  • Test rework, no more default config, need to be specific now

Issues that need to be created:

  • Rework EntityQueryAlter and test according to latest improvements to GroupQueryAlter
  • Fix todos in GroupRoleStorage
  • Fix todo in RelationTypeManager
  • Put calculators in their own module
  • Disallow plugins that define entity_access and concern group entities
  • Disallow plugins that concern group_content entities

kristiaanvandeneynde’s picture

Status: Needs review » Fixed

CRs and follow-ups created, let's get this in.

Status: Fixed » Closed (fixed)

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