-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Utilize Role isAdmin() functionality #225
Changes from all commits
e388600
197cb0e
cbb0ee9
0b2c693
6f6b6fd
e67b43c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,7 @@ | |
* "id", | ||
* "label", | ||
* "weight", | ||
* "is_admin", | ||
* "group_type", | ||
* "group_bundle", | ||
* "group_id", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -255,6 +255,30 @@ public static function getUserMemberships(AccountInterface $user, array $states | |
return static::$cache[$identifier]; | ||
} | ||
|
||
/** | ||
* Returns the group membership for a given user and group. | ||
* | ||
* @param \Drupal\Core\Session\AccountInterface $user | ||
* The user to get the membership for. | ||
* @param \Drupal\Core\Entity\EntityInterface $group | ||
* The group to get the membership for. | ||
* @param array $states | ||
* (optional) Array with the state to return. Defaults to active. | ||
* @param string $field_name | ||
* (optional) The field name associated with the group. | ||
* | ||
* @return \Drupal\og\Entity\OgMembership|NULL | ||
* The OgMembership entity, or NULL if the user is not a member of the | ||
* group. | ||
*/ | ||
public static function getMembership(AccountInterface $user, EntityInterface $group, array $states = [OgMembershipInterface::STATE_ACTIVE], $field_name = NULL) { | ||
foreach (static::getUserMemberships($user, $states, $field_name) as $membership) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a user might have many memberships, so I think better to query for the specific membership we want instead of iterating over all of them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. btw, now that OgMembership is associated only with users maybe we should change the name to just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That works for me! We can have a follow up to rename There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @amitaibu I think we should leave this method as-is for now, as @pfrenssen wrote it. I think we can optimise it later. This keeps it simple, and keeps it statically cached too. If we load single stuff here we might want to look at how some stuff could be shared between getting all memberships and a single one. Maybe a lot of requests would need to get all memberships anyway? |
||
if ($membership->getGroupEntityType() === $group->getEntityTypeId() && $membership->getEntityId() === $group->id()) { | ||
return $membership; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Returns all group IDs associated with the given group content entity. | ||
* | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,6 @@ class OgAccess { | |
*/ | ||
protected static $permissionsCache = []; | ||
|
||
|
||
/** | ||
* Administer permission string. | ||
* | ||
|
@@ -112,18 +111,25 @@ public static function userAccess(EntityInterface $group, $operation, AccountInt | |
// To reduce the number of SQL queries, we cache the user's permissions | ||
// in a static variable. | ||
if (!$pre_alter_cache) { | ||
$permissions = array(); | ||
|
||
foreach (Og::getUserMemberships($user) as $membership) { | ||
$permissions = []; | ||
$user_is_group_admin = FALSE; | ||
|
||
if ($membership = Og::getMembership($user, $group)) { | ||
foreach ($membership->getRoles() as $role) { | ||
// Check for the is_admin flag. | ||
if ($role->isAdmin()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that if a role isAdmin, we don't need to continue iterating. We can just add all the permissions There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. true, I think if the user is_admin, permissions don't really matter. |
||
$user_is_group_admin = TRUE; | ||
break; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you need to keep the |
||
} | ||
|
||
/** @var $role RoleInterface */ | ||
$permissions = array_unique(array_merge($permissions, $role->getPermissions())); | ||
$permissions = array_merge($permissions, $role->getPermissions()); | ||
} | ||
} | ||
|
||
static::setPermissionCache($group, $user, TRUE, $permissions, $cacheable_metadata); | ||
$permissions = array_unique($permissions); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @amitaibu a single There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh, missed that :) |
||
|
||
static::setPermissionCache($group, $user, TRUE, $permissions, $user_is_group_admin, $cacheable_metadata); | ||
} | ||
|
||
if (!$skip_alter && !in_array($operation, $post_alter_cache)) { | ||
|
@@ -138,12 +144,13 @@ public static function userAccess(EntityInterface $group, $operation, AccountInt | |
); | ||
\Drupal::moduleHandler()->alter('og_user_access', $alterable_permissions['permissions'], $cacheable_metadata, $context); | ||
|
||
static::setPermissionCache($group, $user, FALSE, $alterable_permissions['permissions'], $cacheable_metadata); | ||
static::setPermissionCache($group, $user, FALSE, $alterable_permissions['permissions'], $alterable_permissions['is_admin'], $cacheable_metadata); | ||
} | ||
|
||
$altered_permissions = static::getPermissionsCache($group, $user, FALSE); | ||
|
||
$user_is_group_admin = !empty($altered_permissions['permissions'][static::ADMINISTER_GROUP_PERMISSION]); | ||
$user_is_group_admin = !empty($altered_permissions['is_admin']); | ||
|
||
if (($user_is_group_admin && !$ignore_admin) || in_array($operation, $altered_permissions['permissions'])) { | ||
// User is a group admin, and we do not ignore this special permission | ||
// that grants access to all the group permissions. | ||
|
@@ -233,16 +240,19 @@ public static function userAccessEntity($operation, EntityInterface $entity, Acc | |
* Determines if the type of permissions is pre-alter or post-alter. | ||
* @param array $permissions | ||
* Array of permissions to set. | ||
* @param bool @is_admin | ||
* Whether or not the user is a group administrator. | ||
* @param \Drupal\Core\Cache\RefinableCacheableDependencyInterface $cacheable_metadata | ||
* A cacheable metadata object. | ||
*/ | ||
protected static function setPermissionCache(EntityInterface $group, AccountInterface $user, $pre_alter, array $permissions, RefinableCacheableDependencyInterface $cacheable_metadata) { | ||
protected static function setPermissionCache(EntityInterface $group, AccountInterface $user, $pre_alter, array $permissions, $is_admin, RefinableCacheableDependencyInterface $cacheable_metadata) { | ||
$entity_type_id = $group->getEntityTypeId(); | ||
$group_id = $group->id(); | ||
$user_id = $user->id(); | ||
$type = $pre_alter ? 'pre_alter' : 'post_alter'; | ||
|
||
static::$permissionsCache[$entity_type_id][$group_id][$user_id][$type] = [ | ||
'is_admin' => $is_admin, | ||
'permissions' => $permissions, | ||
'cacheable_metadata' => $cacheable_metadata, | ||
]; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,12 +38,20 @@ class OgEntityAccessTest extends KernelTestBase { | |
*/ | ||
protected $user3; | ||
|
||
/** | ||
* @var \Drupal\user\Entity\User | ||
*/ | ||
protected $adminUser; | ||
|
||
/** | ||
* @var \Drupal\entity_test\Entity\EntityTest | ||
*/ | ||
protected $group1; | ||
|
||
/** | ||
* @var \Drupal\entity_test\Entity\EntityTest | ||
*/ | ||
protected $group2; | ||
|
||
/** | ||
* The machine name of the group's bundle. | ||
|
@@ -52,21 +60,34 @@ class OgEntityAccessTest extends KernelTestBase { | |
*/ | ||
protected $groupBundle; | ||
|
||
|
||
/** | ||
* The OG role that has the permission we check for. | ||
* | ||
* @var OgRole | ||
*/ | ||
protected $ogRoleWithPermission; | ||
|
||
/** | ||
* The OG role that has the permission we check for. | ||
* | ||
* @var OgRole | ||
*/ | ||
protected $ogRoleWithPermission2; | ||
|
||
/** | ||
* The OG role that doesn't have the permission we check for. | ||
* | ||
* @var OgRole | ||
*/ | ||
protected $ogRoleWithoutPermission; | ||
|
||
/** | ||
* The OG role that doesn't have the permission we check for. | ||
* | ||
* @var OgRole | ||
*/ | ||
protected $ogAdminRole; | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
|
@@ -100,6 +121,10 @@ protected function setUp() { | |
$this->user3 = User::create(['name' => $this->randomString()]); | ||
$this->user3->save(); | ||
|
||
// Admin user. | ||
$this->adminUser = User::create(['name' => $this->randomString()]); | ||
$this->adminUser->save(); | ||
|
||
|
||
// Define the group content as group. | ||
Og::groupManager()->addGroup('entity_test', $this->groupBundle); | ||
|
@@ -112,6 +137,15 @@ protected function setUp() { | |
]); | ||
$this->group1->save(); | ||
|
||
// Create another group to help test per group/per account permission | ||
// caching. | ||
$this->group2 = EntityTest::create([ | ||
'type' => $this->groupBundle, | ||
'name' => $this->randomString(), | ||
'user_id' => $group_owner->id(), | ||
]); | ||
$this->group2->save(); | ||
|
||
/** @var OgRole ogRoleWithPermission */ | ||
$this->ogRoleWithPermission = OgRole::create(); | ||
$this->ogRoleWithPermission | ||
|
@@ -123,6 +157,16 @@ protected function setUp() { | |
->grantPermission('some_perm') | ||
->save(); | ||
|
||
$this->ogRoleWithPermission2 = OgRole::create(); | ||
$this->ogRoleWithPermission2 | ||
->setId($this->randomMachineName()) | ||
->setLabel($this->randomString()) | ||
->setGroupType($this->group1->getEntityTypeId()) | ||
->setGroupBundle($this->groupBundle) | ||
// Associate an arbitrary permission with the role. | ||
->grantPermission('some_perm_2') | ||
->save(); | ||
|
||
/** @var OgRole ogRoleWithoutPermission */ | ||
$this->ogRoleWithoutPermission = OgRole::create(); | ||
$this->ogRoleWithoutPermission | ||
|
@@ -133,6 +177,15 @@ protected function setUp() { | |
->grantPermission($this->randomMachineName()) | ||
->save(); | ||
|
||
$this->ogAdminRole = OgRole::create(); | ||
$this->ogAdminRole | ||
->setId($this->randomMachineName()) | ||
->setLabel($this->randomString()) | ||
->setGroupType($this->group1->getEntityTypeId()) | ||
->setGroupBundle($this->groupBundle) | ||
->setIsAdmin(TRUE) | ||
->save(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really like the DX of this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed. This reminds me that we should allow setting is-admin in the default roles definition There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, in front of a computer, so here's the reference There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @amitaibu wouldn't this already be supported I'd someone defines a default role that is_admin? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently not, we have protected function createPerBundleRoles($entity_type_id, $bundle_id) {
foreach ($this->getDefaultRoles() as $role_name => $default_properties) {
$properties = [
'group_type' => $entity_type_id,
'group_bundle' => $bundle_id,
'id' => $role_name,
'role_type' => OgRole::getRoleTypeByName($role_name),
]; (no There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh. I see. Cool!
|
||
|
||
|
||
/** @var OgMembership $membership */ | ||
$membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); | ||
|
@@ -143,6 +196,16 @@ protected function setUp() { | |
->addRole($this->ogRoleWithPermission->id()) | ||
->save(); | ||
|
||
// Also create a membership to the other group. From this we can verify that | ||
// permissions are not bled between groups. | ||
$membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); | ||
$membership | ||
->setUser($this->user1->id()) | ||
->setEntityId($this->group2->id()) | ||
->setGroupEntityType($this->group2->getEntityTypeId()) | ||
->addRole($this->ogRoleWithPermission2->id()) | ||
->save(); | ||
|
||
/** @var OgMembership $membership */ | ||
$membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); | ||
$membership | ||
|
@@ -151,6 +214,15 @@ protected function setUp() { | |
->setGroupEntityType($this->group1->getEntityTypeId()) | ||
->addRole($this->ogRoleWithoutPermission->id()) | ||
->save(); | ||
|
||
/** @var OgMembership $membership */ | ||
$membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); | ||
$membership | ||
->setUser($this->adminUser->id()) | ||
->setEntityId($this->group1->id()) | ||
->setGroupEntityType($this->group1->getEntityTypeId()) | ||
->addRole($this->ogAdminRole->id()) | ||
->save(); | ||
} | ||
|
||
/** | ||
|
@@ -159,13 +231,20 @@ protected function setUp() { | |
public function testAccess() { | ||
// A member user. | ||
$this->assertTrue(OgAccess::userAccess($this->group1, 'some_perm', $this->user1)->isAllowed()); | ||
// This user should not have access to 'some_perm_2' as that was only | ||
// assigned to group 2. | ||
$this->assertTrue(OgAccess::userAccess($this->group1, 'some_perm_2', $this->user1)->isForbidden()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great, very important to test this! |
||
|
||
// A member user without the correct role. | ||
$this->assertTrue(OgAccess::userAccess($this->group1, 'some_perm', $this->user2)->isForbidden()); | ||
|
||
// A non-member user. | ||
$this->assertTrue(OgAccess::userAccess($this->group1, 'some_perm', $this->user3)->isForbidden()); | ||
|
||
// Group admin user should have access regardless. | ||
$this->assertTrue(OgAccess::userAccess($this->group1, 'some_perm', $this->adminUser)->isAllowed()); | ||
$this->assertTrue(OgAccess::userAccess($this->group1, $this->randomMachineName(), $this->adminUser)->isAllowed()); | ||
|
||
// Add membership to user 3. | ||
$membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); | ||
$membership | ||
|
@@ -179,4 +258,4 @@ public function testAccess() { | |
} | ||
|
||
|
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@damiankloip This is conceptually changing the logic from:
This is a role that might, or might not have the
administer group
permission. It's just another role like all the othersinto:
This is an admin role. The permissions it has under it are not so important, because the role itself is marked as admin.
I think I prefer the existing solution, because it's taking a similar approach to drupal core, where a role is simply a collection of permissions with some metadata (i.e. the role name).
With this approach we'll now have to check for permission both on the Role level, and on the permissions level.
Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@amitaibu Yes, that is what I'm shifting here.
Hmm, wouldn't we need to do that anyway? As the permissions generated for OG could E.g. apply to one group, and therefore roles have to be check per group like that? The permissions for OG are kind of sub permissions I thought (OgRole !== global administer groups permission)? So this utilises all the isAdmin() stuff that is already a part of core Roles. So Core is already doing this with it's roles. I.e. if it's marked as a full administrator role, you can do anything. That same assumption applies on the OG level. E.g. a user membership has a role attached that is an admin role (is_admin = true) and they can then be a group admin. So all checks in OgAccess with check that anyway. E.g Role::hasPermission() (used by OgRole) will also check if the role isAdmin() first.
See
OgAccess::userAccess
- the global admin perm is checked, then if that is not true, we get user memberships and check those.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
E.g. see
user.role.administrator.yml
config in a D8 install.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could also adopt fromthing like core's RoleStorage, a
isPermissionInRoles
method. That would check each role for x permission or admin.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@amitaibu OH, so OgAccess::userAccess collects all permissions, which would be specific to a node bundle for example? I thought users can just have an admin role for e.g. 1 group and not another?
Basically,
Og::userAccess
does:but it caches them per group, per user. This doesn't seem correct. Should we only be checking the roles/permissions for the current group membership only?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First, just to make sure we are on the same page, in OG7:
administer groups
site-wide permission, which allows a user to administer all groupsadminister group
OG permission, which allows a user to administer a specific group they might have the role in.So it seems that in D8,
Role
has evolved a bit and now holds a booleanis_admin
. As OG is following core's logic this means that in OG8 we should remove theadminister group
OG permission in favor of theis_admin
flag.Also just to clarify in general how we deal with the permissions:
member
role in Group A, and aadminister
role in Group B.allow
to grant access.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, and it's awesome having you back :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @amitaibu ! And yes, that is pretty much how I interpreted the use of roles and permissions. Which is useful :)