From 56ab821e40eff1a687835b16069c20345211951a Mon Sep 17 00:00:00 2001 From: Pieter Frenssen Date: Wed, 12 Aug 2020 18:28:13 +0300 Subject: [PATCH 1/9] Start to document how OG handles access and permissions. --- docs/access.md | 134 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 docs/access.md diff --git a/docs/access.md b/docs/access.md new file mode 100644 index 000000000..a0f2206c5 --- /dev/null +++ b/docs/access.md @@ -0,0 +1,134 @@ +Access control for groups and group content +=========================================== + +Controlling access to groups and group content is one of the most important +aspects of building a group based project. Group based access is more complex +than the standard role and permission based access system built into Drupal core +so Organic Groups has extended the core functionality to make it more flexible +and, indeed, _organic_ for developers to design their access control systems. + +Group level permissions +----------------------- + +The first line of defense is the group level access control provided by OG. +Group level permissions apply to the group as a whole, and OG defines a number +of permissions that control basic group and membership management tasks such as: +- Administration permissions such as `update group`, `delete group`, `manage + members` and `approve and deny subscription`. +- Membership permissions such as `subscribe` and `subscribe without approval` + which can be granted to non-members. + +Developers can define their own group level permissions by implementing an event +listener that subscribes to the `og.permission` event and instantiating +`GroupPermission` objects with the properties of the permission. + +As an example let's define a permission that would allow an administrator to +make a group private or public - this could be used in a project that has +private groups that would only accept new members that are invited by existing +members: + +```php + [['provideGroupLevelPermissions']], + ]; + } + + public function provideGroupLevelPermissions(PermissionEventInterface $event): void { + $event->setPermission( + new GroupPermission([ + // The unique machine name of the permission, similar to a Drupal core + // permission. + 'name' => 'set group privacy', + // The human readable permission title, to show to site builders in the UI. + 'title' => $this->t('Set group privacy'), + // An optional description providing extra information. + 'description' => $this->t('Users can only join a private group when invited.'), + // The roles to which this permission applies by default when a new + // group is created. + 'default roles' => [OgRoleInterface::ADMINISTRATOR], + // Whether or not this permission has security implications and should + // be restricted to trusted users only. + 'restrict access' => TRUE, + ]) + ); + } + +} +``` + +The list of group level permissions that are provided out of the box by Organic +Groups can be found in +`\Drupal\og\EventSubscriber\OgEventSubscriber::provideDefaultOgPermissions()`. + + +Group content entity operation permissions +------------------------------------------ + +In addition to group level permissions we also need to control access to CRUD +operations on group content entities. Depending on the group requirements the +creation, updating and deletion of group content is restricted to certain roles +within the group. + +In Drupal core the entity CRUD operations are defined as simple strings (e.g. +an for an 'article' node type we would have the permission `delete own article +content`). + +OG not only supports nodes but all kinds of entities, and the simple string +based permissions from Drupal core have proved to be difficult to use for +operations that need to be applicable across multiple entity types. Some typical +use cases are that a group administrator should be able to edit and delete all +group content, regardless of entity type. Also a normal member of a group might +have the permission to edit their own content, but not the content of other +members. + +The permission system from Drupal core does not lend itself well to these use +cases since they are simple unstructured strings and we cannot reliably derive +the entity type and operation from them. For example the permission `delete own +article content` gives a user the permission to delete nodes of type 'article' +which were created by themselves. This permission contains the words 'delete' +and 'own' so it would be possible to come up with an algorithm that infers the +operation and the scope, but the bundle and entity type can not be derived +easily. In fact the entity type 'node' doesn't even appear in the string! + +In OG this is solved by defining group content entity operation (CRUD) +permissions using a structured object: `GroupContentOperationPermission`. The +above permission would be defined as follows, which encodes all the data we need +to determine the entity type, bundle, operation, ownership (whether or not a +user is performing the operation on their own entity), etc: + +```php +$permission = new \Drupal\og\GroupContentOperationPermission([ + 'entity_type' => 'node', + 'bundle' => 'article', + 'name' => "delete own article content", + 'title' => $this->t('%type_name: Delete own content', ['type_name' => 'article']), + 'operation' => 'delete', + 'owner' => TRUE, +]); +``` + +These permissions are defined in the same way as the group level permissions, in +an event listener for the `og.permissions` event. Here are some example how OG +sets these permissions: + +- `OgEventSubscriber::getDefaultEntityOperationPermissions()`: creates a generic + set of permissions for every group content type. These can be overridden by + custom modules if needed. +- `OgEventSubscriber::provideDefaultNodePermissions()`: an example of how the + generic permissions can be overridden. This ensures that the same permission + names are used for the Node entity type as used by core. From 59f91a4a034e2507f4b5e2d43ba7ae35d7eb736f Mon Sep 17 00:00:00 2001 From: Pieter Frenssen Date: Wed, 12 Aug 2020 20:18:24 +0300 Subject: [PATCH 2/9] Update documentation. --- docs/access.md | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/docs/access.md b/docs/access.md index a0f2206c5..84a544d03 100644 --- a/docs/access.md +++ b/docs/access.md @@ -123,7 +123,7 @@ $permission = new \Drupal\og\GroupContentOperationPermission([ ``` These permissions are defined in the same way as the group level permissions, in -an event listener for the `og.permissions` event. Here are some example how OG +an event listener for the `og.permissions` event. Here are some examples how OG sets these permissions: - `OgEventSubscriber::getDefaultEntityOperationPermissions()`: creates a generic @@ -132,3 +132,49 @@ sets these permissions: - `OgEventSubscriber::provideDefaultNodePermissions()`: an example of how the generic permissions can be overridden. This ensures that the same permission names are used for the Node entity type as used by core. + + +Granting permissions to users +----------------------------- + +Similar to Drupal core, permissions are not directly assigned to users in OG, +but they are assigned to roles, and a user can have one or more roles in a +group. + +The role data is stored in a config entity type named `OgRole`, and whenever a +new group type is created, a number of roles will automatically be created: + +- `member` and `non-member`: these two roles are required for every group, they + indicate whether or not a user is a member. +- `administrator`: this role is not strictly required but is created by default + by OG because it is considered to be generally useful for most groups. +- Developers can choose to provide additional default roles by listening to the + `og.default_role` event. + +Whenever a default role is created, it will automatically inherit the +permissions that are assigned to the role in their permission declaration (see +the `default role` property above which takes an array of roles to which these +permissions should be applied). + +To manually assign a permission to a role for a certain group, it can be done as +follows: + +```php +$admin_role = OgRole::loadByGroupAndName($this->group, OgRoleInterface::ADMINISTRATOR); +$admin_role->grantPermission('my permission')->save(); +```` + +Similarly, an existing permission can be removed from a role by calling +`$admin_role->revokePermission('my permission')` and saving the role entity. + +For more information on how `OgRole` objects are handled, check the following +methods: +- `\Drupal\og\GroupTypeManager::addGroup()`: this code is responsible for + creating a new group type and will create the roles as part of it. +- `\Drupal\og\OgRoleManager::getDefaultRoles()`: creates the default roles and + fires the event listener that modules can hook into the provide additional + default roles. +- `\Drupal\og\EventSubscriber\OgEventSubscriber::provideDefaultRoles()`: OG's + own implementation of the event listener, which provides the `administrator` + role. + From 4b057f2ced10472d6258f3ea3f58102668d52096 Mon Sep 17 00:00:00 2001 From: Pieter Frenssen Date: Thu, 13 Aug 2020 10:22:46 +0300 Subject: [PATCH 3/9] Update documentation. --- docs/access.md | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/docs/access.md b/docs/access.md index 84a544d03..fddc29527 100644 --- a/docs/access.md +++ b/docs/access.md @@ -160,6 +160,8 @@ To manually assign a permission to a role for a certain group, it can be done as follows: ```php +// OgRole::loadByGroupAndName() is the easiest way to load a specific role for +// a given group. $admin_role = OgRole::loadByGroupAndName($this->group, OgRoleInterface::ADMINISTRATOR); $admin_role->grantPermission('my permission')->save(); ```` @@ -178,3 +180,72 @@ methods: own implementation of the event listener, which provides the `administrator` role. + +Checking if a user has a certain permission in a group +------------------------------------------------------ + +It would be natural to think that checking a permission would be a simple matter +of loading the `OgRole` entity and calling `$role->hasPermission()` on it, but +this is not sufficient. There are a number of additional things to consider: + +- The super user (user ID 1) has full permissions. +- OG has a configuration option to allow full access to group owners. +- Users that have the global permission `administer organic groups` have all + permissions in all groups. +- The role can have the `is_admin` flag set which will grant all permissions. +- Modules can alter permissions depending on their own requirements. + +Organic Groups provides a service that will perform these checks. To determine +whether the currently logged in user has for example the `manage members` +permission on a given group entity: + +```php +// Load some custom group. +$group = \Drupal::entityTypeManager()->getStorage('my_group_type')->load($some_id); + +/** @var \Drupal\og\OgAccessInterface $og_access */ +$og_access = \Drupal::service('og.access'); +$access_result = $og_access->userAccess($group, 'manage members'); + +// An access result object is returned including full cacheability metadata. In +// order to get the access as a simple boolean value, call `::isAllowed()`. +if ($access_result->isAllowed()) { + // The user has permission. +} +``` + +For projects that have a large number of group types and group content types +there is also a convenient method `::userAccessEntity()` to discover if a user +has a group level permission on any given entity, being a group, group content, +or even something that is not related to any group: + +```php +// Load some entity, which might be a group, group content, or something not +// related to any group. +$entity = \Drupal::entityTypeManager()->getStorage('my_entity_type')->load($some_id); + +/** @var \Drupal\og\OgAccessInterface $og_access */ +$og_access = \Drupal::service('og.access'); +$access_result = $og_access->userAccessEntity('manage members', $entity); + +// An access result object is returned including full cacheability metadata. In +// order to get the access as a simple boolean value, call `::isAllowed()`. +if ($access_result->isAllowed()) { + // The user has permission. +} +``` + +The above example will do a discovery to find out if the passed in entity is a +group or group content, and will loop over all associated groups to determine +access. While this is very convenient this also comes with a performance impact, +so it is recommended to use it only in cases where the faster `::userAccess()` +is not applicable. + + +Checking if a user can perform an entity operation on group content +------------------------------------------------------------------- + + +Altering permissions +-------------------- + From e51197bb98c5f3bbc68e7f9df59cc34623510920 Mon Sep 17 00:00:00 2001 From: Pieter Frenssen Date: Thu, 13 Aug 2020 11:37:58 +0300 Subject: [PATCH 4/9] Update documentation. --- docs/access.md | 114 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/docs/access.md b/docs/access.md index fddc29527..a1b52b20f 100644 --- a/docs/access.md +++ b/docs/access.md @@ -245,7 +245,121 @@ is not applicable. Checking if a user can perform an entity operation on group content ------------------------------------------------------------------- +OG extends the entity access control system from Drupal core so checking access +on an entity operation is as simple as this: + +```php +// Check if the given user can edit the entity, which is a group content entity. +$access_result = $group_content_entity->access('update', $user); +``` + +Behind the scenes, OG implements `hook_access()` and delegates the access check +to the `OgAccess` service, so within the context of group content this is +equivalent to calling the following: + +```php +/** @var \Drupal\og\OgAccessInterface $og_access */ +$og_access = \Drupal::service('og.access'); +$access_result = $og_access->userAccessEntityOperation('update', $group_content_entity, $user); +``` + +There is also a faster way to get the same result, in case you know beforehand +to which group the group content entity belongs. The following example is more +efficient since it doesn't need to do an expensive discovery of the groups to +which the entity belongs: + +```php +// In case we know the group entity we can use the faster method: +$group_entity = \Drupal::entityTypeManager()->getStorage('my_group_type')->load($some_id); + +/** @var \Drupal\og\OgAccessInterface $og_access */ +$og_access = \Drupal::service('og.access'); +$access_result = $og_access->userAccessGroupContentEntityOperation('update', $group_entity, $group_content_entity, $user); +``` + Altering permissions -------------------- +There are many use cases where permissions should be altered under some +circumstances to fulfill business requirements. OG offers ways for modules to +hook into the permission system and alter the access result. + +Modules can implement `hook_og_user_access_alter()` to alter group level +permissions. Here is an example that implements a use case where groups can only +be deleted if they are unpublished. This functionality can be toggled off by +site administrators in the site configuration, so the example also demonstrates +how to alter the cacheability metadata to include the config setting. The access +result is different if this option is turned on or off, so this needs to be +included in the cache metadata. + +```php +function mymodule_og_user_access_alter(array &$permissions, CacheableMetadata $cacheable_metadata, array $context): void { + // Retrieve the module configuration. + $config = \Drupal::config('mymodule.settings'); + + // Check if the site is configured to allow deletion of published groups. + $published_groups_can_be_deleted = $config->get('delete_published_groups'); + + // If deletion is not allowed and the group is published, revoke the + // permission. + $group = $context['group']; + if ($group instanceof EntityPublishedInterface && !$group->isPublished() && !$published_groups_can_be_deleted) { + $key = array_search(OgAccess::DELETE_GROUP_PERMISSION, $permissions); + if ($key !== FALSE) { + unset($permissions[$key]); + } + } + + // Since our access result depends on our custom module configuration, we need + // to add it to the cache metadata. + $cacheable_metadata->addCacheableDependency($config); +} +``` + +In addition to altering group level permissions, OG also allows to alter access +to group content entity operations, this time using an event listener. + +```php + [['moderatorsCanManageComments']], + ]; + } + + public function moderatorsCanManageComments(GroupContentEntityOperationAccessEventInterface $event): void { + $is_comment = $event->getGroupContent()->getEntityTypeId() === 'comment'; + $user_can_moderate_comments = $event->getUser()->hasPermission('edit and delete comments in all groups'); + + if ($is_comment && $user_can_moderate_comments) { + $event->grantAccess(); + } + } + +} +``` + +Please note that this follows the same principles of the Drupal core entity +access handlers. Access will be granted only if at least one of the subscribers +or other properties grants access (like having the `administer organic groups` +permission). If any event listener __denies__ access, then this will be +considered as a hard deny, and cannot be overruled. This might have some +unexpected consequences; for example if group content is published in multiple +groups, and a user has access to a permission in all groups, except one in which +access is forbidden, then `OgAccess::userAccessEntityOperation()` will return +access denied. + +In most cases this can be solved by only granting access to a permission when +required, and remaining neutral if not. Alternatively check access to +individual groups using `OgAccess::userAccessGroupContentEntityOperation()` - +this will only return access denied for the specific group where access has been +forbidden, while still allowing access for all others. From 14435375d2df17cf447d0564917672fc282bc51d Mon Sep 17 00:00:00 2001 From: Pieter Frenssen Date: Thu, 13 Aug 2020 11:52:41 +0300 Subject: [PATCH 5/9] Update documentation. --- docs/access.md | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/docs/access.md b/docs/access.md index a1b52b20f..29335106d 100644 --- a/docs/access.md +++ b/docs/access.md @@ -2,16 +2,17 @@ Access control for groups and group content =========================================== Controlling access to groups and group content is one of the most important -aspects of building a group based project. Group based access is more complex -than the standard role and permission based access system built into Drupal core -so Organic Groups has extended the core functionality to make it more flexible -and, indeed, _organic_ for developers to design their access control systems. +aspects of building a group based project. Having separate access rules for +different groups and the content in them is more complex than the standard role +and permission based access system built into Drupal core so Organic Groups has +extended the core functionality to make it more flexible and, indeed, _organic_ +for developers to design their access control systems. Group level permissions ----------------------- -The first line of defense is the group level access control provided by OG. -Group level permissions apply to the group as a whole, and OG defines a number +The first line of defense is group level access control. +Group level permissions apply to the group as a whole. OG ships with a number of permissions that control basic group and membership management tasks such as: - Administration permissions such as `update group`, `delete group`, `manage members` and `approve and deny subscription`. @@ -21,7 +22,7 @@ of permissions that control basic group and membership management tasks such as: Developers can define their own group level permissions by implementing an event listener that subscribes to the `og.permission` event and instantiating `GroupPermission` objects with the properties of the permission. - + As an example let's define a permission that would allow an administrator to make a group private or public - this could be used in a project that has private groups that would only accept new members that are invited by existing @@ -57,7 +58,7 @@ class OgEventSubscriber implements EventSubscriberInterface { // The human readable permission title, to show to site builders in the UI. 'title' => $this->t('Set group privacy'), // An optional description providing extra information. - 'description' => $this->t('Users can only join a private group when invited.'), + 'description' => $this->t('Users can only join a private group when invited by an existing member.'), // The roles to which this permission applies by default when a new // group is created. 'default roles' => [OgRoleInterface::ADMINISTRATOR], @@ -70,7 +71,7 @@ class OgEventSubscriber implements EventSubscriberInterface { } ``` - + The list of group level permissions that are provided out of the box by Organic Groups can be found in `\Drupal\og\EventSubscriber\OgEventSubscriber::provideDefaultOgPermissions()`. @@ -86,7 +87,7 @@ within the group. In Drupal core the entity CRUD operations are defined as simple strings (e.g. an for an 'article' node type we would have the permission `delete own article -content`). +content`). OG not only supports nodes but all kinds of entities, and the simple string based permissions from Drupal core have proved to be difficult to use for @@ -97,10 +98,10 @@ have the permission to edit their own content, but not the content of other members. The permission system from Drupal core does not lend itself well to these use -cases since they are simple unstructured strings and we cannot reliably derive -the entity type and operation from them. For example the permission `delete own -article content` gives a user the permission to delete nodes of type 'article' -which were created by themselves. This permission contains the words 'delete' +cases since we cannot reliably derive the entity type and operation from these +simple string based permissions. For example the permission `delete own article +content` gives a user the permission to delete nodes of type 'article' which +were created by themselves. This permission string contains the words 'delete' and 'own' so it would be possible to come up with an algorithm that infers the operation and the scope, but the bundle and entity type can not be derived easily. In fact the entity type 'node' doesn't even appear in the string! @@ -130,8 +131,9 @@ sets these permissions: set of permissions for every group content type. These can be overridden by custom modules if needed. - `OgEventSubscriber::provideDefaultNodePermissions()`: an example of how the - generic permissions can be overridden. This ensures that the same permission - names are used for the Node entity type as used by core. + generic permissions can be overridden. This ensures that we can use the same + permission names for the Node entity type in our group content as the ones + used by core. Granting permissions to users @@ -362,4 +364,4 @@ In most cases this can be solved by only granting access to a permission when required, and remaining neutral if not. Alternatively check access to individual groups using `OgAccess::userAccessGroupContentEntityOperation()` - this will only return access denied for the specific group where access has been -forbidden, while still allowing access for all others. +forbidden, while still allowing access for all others. From 0d23e61f62fd561a9412e64acaf2657edd17109a Mon Sep 17 00:00:00 2001 From: Pieter Frenssen Date: Sat, 15 Aug 2020 16:49:04 +0300 Subject: [PATCH 6/9] Apply suggestions from code review Co-authored-by: Maarten Segers --- docs/access.md | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/docs/access.md b/docs/access.md index 29335106d..6a9039f05 100644 --- a/docs/access.md +++ b/docs/access.md @@ -20,7 +20,7 @@ of permissions that control basic group and membership management tasks such as: which can be granted to non-members. Developers can define their own group level permissions by implementing an event -listener that subscribes to the `og.permission` event and instantiating +listener that subscribes to the `PermissionEventInterface::EVENT_NAME` event and instantiating `GroupPermission` objects with the properties of the permission. As an example let's define a permission that would allow an administrator to @@ -85,7 +85,7 @@ operations on group content entities. Depending on the group requirements the creation, updating and deletion of group content is restricted to certain roles within the group. -In Drupal core the entity CRUD operations are defined as simple strings (e.g. +Drupal core defines the entity CRUD operations as simple strings (e.g. an for an 'article' node type we would have the permission `delete own article content`). @@ -97,7 +97,7 @@ group content, regardless of entity type. Also a normal member of a group might have the permission to edit their own content, but not the content of other members. -The permission system from Drupal core does not lend itself well to these use +Drupal core's permission system doesn't lend itself well to these use cases since we cannot reliably derive the entity type and operation from these simple string based permissions. For example the permission `delete own article content` gives a user the permission to delete nodes of type 'article' which @@ -106,7 +106,7 @@ and 'own' so it would be possible to come up with an algorithm that infers the operation and the scope, but the bundle and entity type can not be derived easily. In fact the entity type 'node' doesn't even appear in the string! -In OG this is solved by defining group content entity operation (CRUD) +OG solves this by defining group content entity operation (CRUD) permissions using a structured object: `GroupContentOperationPermission`. The above permission would be defined as follows, which encodes all the data we need to determine the entity type, bundle, operation, ownership (whether or not a @@ -129,7 +129,7 @@ sets these permissions: - `OgEventSubscriber::getDefaultEntityOperationPermissions()`: creates a generic set of permissions for every group content type. These can be overridden by - custom modules if needed. + custom modules as shown below. - `OgEventSubscriber::provideDefaultNodePermissions()`: an example of how the generic permissions can be overridden. This ensures that we can use the same permission names for the Node entity type in our group content as the ones @@ -144,7 +144,7 @@ but they are assigned to roles, and a user can have one or more roles in a group. The role data is stored in a config entity type named `OgRole`, and whenever a -new group type is created, a number of roles will automatically be created: +new group type is created, the following roles will automatically be created: - `member` and `non-member`: these two roles are required for every group, they indicate whether or not a user is a member. @@ -155,7 +155,7 @@ new group type is created, a number of roles will automatically be created: Whenever a default role is created, it will automatically inherit the permissions that are assigned to the role in their permission declaration (see -the `default role` property above which takes an array of roles to which these +the `default roles` property above which takes an array of roles to which these permissions should be applied). To manually assign a permission to a role for a certain group, it can be done as @@ -169,7 +169,7 @@ $admin_role->grantPermission('my permission')->save(); ```` Similarly, an existing permission can be removed from a role by calling -`$admin_role->revokePermission('my permission')` and saving the role entity. +`$admin_role->revokePermission('my permission')` and saving the `OgRole` entity. For more information on how `OgRole` objects are handled, check the following methods: @@ -179,7 +179,7 @@ methods: fires the event listener that modules can hook into the provide additional default roles. - `\Drupal\og\EventSubscriber\OgEventSubscriber::provideDefaultRoles()`: OG's - own implementation of the event listener, which provides the `administrator` + own implementation of the event listener, which provides the default `administrator` role. @@ -209,7 +209,7 @@ $group = \Drupal::entityTypeManager()->getStorage('my_group_type')->load($some_i $og_access = \Drupal::service('og.access'); $access_result = $og_access->userAccess($group, 'manage members'); -// An access result object is returned including full cacheability metadata. In +// An AccessResult object is returned including full cacheability metadata. In // order to get the access as a simple boolean value, call `::isAllowed()`. if ($access_result->isAllowed()) { // The user has permission. @@ -230,14 +230,14 @@ $entity = \Drupal::entityTypeManager()->getStorage('my_entity_type')->load($some $og_access = \Drupal::service('og.access'); $access_result = $og_access->userAccessEntity('manage members', $entity); -// An access result object is returned including full cacheability metadata. In +// An AccessResult object is returned including full cacheability metadata. In // order to get the access as a simple boolean value, call `::isAllowed()`. if ($access_result->isAllowed()) { // The user has permission. } ``` -The above example will do a discovery to find out if the passed in entity is a +**Caution**: The above example will do a discovery to find out if the passed in entity is a group or group content, and will loop over all associated groups to determine access. While this is very convenient this also comes with a performance impact, so it is recommended to use it only in cases where the faster `::userAccess()` @@ -247,7 +247,7 @@ is not applicable. Checking if a user can perform an entity operation on group content ------------------------------------------------------------------- -OG extends the entity access control system from Drupal core so checking access +OG extends Drupal core's entity access control system so checking access on an entity operation is as simple as this: ```php @@ -287,6 +287,9 @@ There are many use cases where permissions should be altered under some circumstances to fulfill business requirements. OG offers ways for modules to hook into the permission system and alter the access result. + +### Alter group permissions + Modules can implement `hook_og_user_access_alter()` to alter group level permissions. Here is an example that implements a use case where groups can only be deleted if they are unpublished. This functionality can be toggled off by @@ -319,6 +322,9 @@ function mymodule_og_user_access_alter(array &$permissions, CacheableMetadata $c } ``` + +### Alter group content permissions + In addition to altering group level permissions, OG also allows to alter access to group content entity operations, this time using an event listener. From cf339441e3b5598df9f4a8423e3801320f4a44e0 Mon Sep 17 00:00:00 2001 From: Pieter Frenssen Date: Sat, 15 Aug 2020 17:03:17 +0300 Subject: [PATCH 7/9] Consistently use 'Organic Groups' instead of 'OG'. --- docs/access.md | 78 ++++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/docs/access.md b/docs/access.md index 6a9039f05..bef1d461b 100644 --- a/docs/access.md +++ b/docs/access.md @@ -11,17 +11,17 @@ for developers to design their access control systems. Group level permissions ----------------------- -The first line of defense is group level access control. -Group level permissions apply to the group as a whole. OG ships with a number -of permissions that control basic group and membership management tasks such as: +The first line of defense is group level access control. Group level permissions +apply to the group as a whole. Organic Groups ships with a number of +permissions that control basic group and membership management tasks such as: - Administration permissions such as `update group`, `delete group`, `manage members` and `approve and deny subscription`. - Membership permissions such as `subscribe` and `subscribe without approval` which can be granted to non-members. Developers can define their own group level permissions by implementing an event -listener that subscribes to the `PermissionEventInterface::EVENT_NAME` event and instantiating -`GroupPermission` objects with the properties of the permission. +listener that subscribes to the `PermissionEventInterface::EVENT_NAME` event and +instantiating `GroupPermission` objects with the properties of the permission. As an example let's define a permission that would allow an administrator to make a group private or public - this could be used in a project that has @@ -85,28 +85,27 @@ operations on group content entities. Depending on the group requirements the creation, updating and deletion of group content is restricted to certain roles within the group. -Drupal core defines the entity CRUD operations as simple strings (e.g. -an for an 'article' node type we would have the permission `delete own article -content`). +Drupal core defines the entity CRUD operations as simple strings (e.g. for an +'article' node type we would have the permission `delete own article content`). -OG not only supports nodes but all kinds of entities, and the simple string -based permissions from Drupal core have proved to be difficult to use for +Organic Groups not only supports nodes but all kinds of entities, and the simple +string based permissions from Drupal core have proved to be difficult to use for operations that need to be applicable across multiple entity types. Some typical use cases are that a group administrator should be able to edit and delete all group content, regardless of entity type. Also a normal member of a group might have the permission to edit their own content, but not the content of other members. -Drupal core's permission system doesn't lend itself well to these use -cases since we cannot reliably derive the entity type and operation from these -simple string based permissions. For example the permission `delete own article +Drupal core's permission system doesn't lend itself well to these use cases +since we cannot reliably derive the entity type and operation from these simple +string based permissions. For example the permission `delete own article content` gives a user the permission to delete nodes of type 'article' which were created by themselves. This permission string contains the words 'delete' and 'own' so it would be possible to come up with an algorithm that infers the operation and the scope, but the bundle and entity type can not be derived easily. In fact the entity type 'node' doesn't even appear in the string! -OG solves this by defining group content entity operation (CRUD) +Organic Groups solves this by defining group content entity operation (CRUD) permissions using a structured object: `GroupContentOperationPermission`. The above permission would be defined as follows, which encodes all the data we need to determine the entity type, bundle, operation, ownership (whether or not a @@ -124,8 +123,8 @@ $permission = new \Drupal\og\GroupContentOperationPermission([ ``` These permissions are defined in the same way as the group level permissions, in -an event listener for the `og.permissions` event. Here are some examples how OG -sets these permissions: +an event listener for the `og.permissions` event. Here are some examples how +Organic Groups sets these permissions: - `OgEventSubscriber::getDefaultEntityOperationPermissions()`: creates a generic set of permissions for every group content type. These can be overridden by @@ -139,9 +138,9 @@ sets these permissions: Granting permissions to users ----------------------------- -Similar to Drupal core, permissions are not directly assigned to users in OG, -but they are assigned to roles, and a user can have one or more roles in a -group. +Similar to Drupal core, permissions are not directly assigned to users in +Organic Groups, but they are assigned to roles, and a user can have one or more +roles in a group. The role data is stored in a config entity type named `OgRole`, and whenever a new group type is created, the following roles will automatically be created: @@ -149,7 +148,8 @@ new group type is created, the following roles will automatically be created: - `member` and `non-member`: these two roles are required for every group, they indicate whether or not a user is a member. - `administrator`: this role is not strictly required but is created by default - by OG because it is considered to be generally useful for most groups. + by Organic Groups because it is considered to be generally useful for most + groups. - Developers can choose to provide additional default roles by listening to the `og.default_role` event. @@ -178,9 +178,9 @@ methods: - `\Drupal\og\OgRoleManager::getDefaultRoles()`: creates the default roles and fires the event listener that modules can hook into the provide additional default roles. -- `\Drupal\og\EventSubscriber\OgEventSubscriber::provideDefaultRoles()`: OG's - own implementation of the event listener, which provides the default `administrator` - role. +- `\Drupal\og\EventSubscriber\OgEventSubscriber::provideDefaultRoles()`: Organic + Groups' own implementation of the event listener, which provides the default + `administrator` role. Checking if a user has a certain permission in a group @@ -191,7 +191,8 @@ of loading the `OgRole` entity and calling `$role->hasPermission()` on it, but this is not sufficient. There are a number of additional things to consider: - The super user (user ID 1) has full permissions. -- OG has a configuration option to allow full access to group owners. +- Organic Groups has a configuration option to allow full access to group + owners. - Users that have the global permission `administer organic groups` have all permissions in all groups. - The role can have the `is_admin` flag set which will grant all permissions. @@ -237,27 +238,27 @@ if ($access_result->isAllowed()) { } ``` -**Caution**: The above example will do a discovery to find out if the passed in entity is a -group or group content, and will loop over all associated groups to determine -access. While this is very convenient this also comes with a performance impact, -so it is recommended to use it only in cases where the faster `::userAccess()` -is not applicable. +**Caution**: The above example will do a discovery to find out if the passed in +entity is a group or group content, and will loop over all associated groups to +determine access. While this is very convenient this also comes with a +performance impact, so it is recommended to use it only in cases where the +faster `::userAccess()` is not applicable. Checking if a user can perform an entity operation on group content ------------------------------------------------------------------- -OG extends Drupal core's entity access control system so checking access -on an entity operation is as simple as this: +Organic Groups extends Drupal core's entity access control system so checking +access on an entity operation is as simple as this: ```php // Check if the given user can edit the entity, which is a group content entity. $access_result = $group_content_entity->access('update', $user); ``` -Behind the scenes, OG implements `hook_access()` and delegates the access check -to the `OgAccess` service, so within the context of group content this is -equivalent to calling the following: +Behind the scenes, Organic Groups implements `hook_access()` and delegates the +access check to the `OgAccess` service, so within the context of group content +this is equivalent to calling the following: ```php /** @var \Drupal\og\OgAccessInterface $og_access */ @@ -284,8 +285,8 @@ Altering permissions -------------------- There are many use cases where permissions should be altered under some -circumstances to fulfill business requirements. OG offers ways for modules to -hook into the permission system and alter the access result. +circumstances to fulfill business requirements. Organic Groups offers ways for +modules to hook into the permission system and alter the access result. ### Alter group permissions @@ -325,8 +326,9 @@ function mymodule_og_user_access_alter(array &$permissions, CacheableMetadata $c ### Alter group content permissions -In addition to altering group level permissions, OG also allows to alter access -to group content entity operations, this time using an event listener. +In addition to altering group level permissions, Organic Groups also allows to +alter access to group content entity operations, this time using an event +listener. ```php Date: Tue, 1 Sep 2020 11:31:33 +0300 Subject: [PATCH 8/9] Link to the access documentation from the main README. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index abd784bc0..5f8b02c07 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,10 @@ Og::groupTypeManager()->addGroup('node', 'page'); Og::createField(OgGroupAudienceHelperInterface::DEFAULT_FIELD, 'node', 'news'); ``` +## Access control + +See [Access control for groups and group content](docs/access.md). + ## DRUPAL CONSOLE INTEGRATION The Drupal 8 branch integrates with Drupal Console to do actions which used by developers only. The supported actions are: From c928e3409d3106b727bd83b26a4215ad5ce5184b Mon Sep 17 00:00:00 2001 From: Pieter Frenssen Date: Tue, 1 Sep 2020 11:32:07 +0300 Subject: [PATCH 9/9] Shorten Organic Groups to OG. --- docs/access.md | 69 ++++++++++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/docs/access.md b/docs/access.md index bef1d461b..ac132fba8 100644 --- a/docs/access.md +++ b/docs/access.md @@ -4,16 +4,16 @@ Access control for groups and group content Controlling access to groups and group content is one of the most important aspects of building a group based project. Having separate access rules for different groups and the content in them is more complex than the standard role -and permission based access system built into Drupal core so Organic Groups has -extended the core functionality to make it more flexible and, indeed, _organic_ -for developers to design their access control systems. +and permission based access system built into Drupal core so Organic Groups (OG) +has extended the core functionality to make it more flexible and, indeed, +_organic_ for developers to design their access control systems. Group level permissions ----------------------- The first line of defense is group level access control. Group level permissions -apply to the group as a whole. Organic Groups ships with a number of -permissions that control basic group and membership management tasks such as: +apply to the group as a whole. OG ships with a number of permissions that +control basic group and membership management tasks such as: - Administration permissions such as `update group`, `delete group`, `manage members` and `approve and deny subscription`. - Membership permissions such as `subscribe` and `subscribe without approval` @@ -72,8 +72,8 @@ class OgEventSubscriber implements EventSubscriberInterface { } ``` -The list of group level permissions that are provided out of the box by Organic -Groups can be found in +The list of group level permissions that are provided out of the box by OG can +be found in `\Drupal\og\EventSubscriber\OgEventSubscriber::provideDefaultOgPermissions()`. @@ -88,8 +88,8 @@ within the group. Drupal core defines the entity CRUD operations as simple strings (e.g. for an 'article' node type we would have the permission `delete own article content`). -Organic Groups not only supports nodes but all kinds of entities, and the simple -string based permissions from Drupal core have proved to be difficult to use for +OG not only supports nodes but all kinds of entities, and the simple string +based permissions from Drupal core have proved to be difficult to use for operations that need to be applicable across multiple entity types. Some typical use cases are that a group administrator should be able to edit and delete all group content, regardless of entity type. Also a normal member of a group might @@ -105,11 +105,11 @@ and 'own' so it would be possible to come up with an algorithm that infers the operation and the scope, but the bundle and entity type can not be derived easily. In fact the entity type 'node' doesn't even appear in the string! -Organic Groups solves this by defining group content entity operation (CRUD) -permissions using a structured object: `GroupContentOperationPermission`. The -above permission would be defined as follows, which encodes all the data we need -to determine the entity type, bundle, operation, ownership (whether or not a -user is performing the operation on their own entity), etc: +OG solves this by defining group content entity operation (CRUD) permissions +using a structured object: `GroupContentOperationPermission`. The above +permission would be defined as follows, which encodes all the data we need to +determine the entity type, bundle, operation, ownership (whether or not a user +is performing the operation on their own entity), etc: ```php $permission = new \Drupal\og\GroupContentOperationPermission([ @@ -124,7 +124,7 @@ $permission = new \Drupal\og\GroupContentOperationPermission([ These permissions are defined in the same way as the group level permissions, in an event listener for the `og.permissions` event. Here are some examples how -Organic Groups sets these permissions: +OG sets these permissions: - `OgEventSubscriber::getDefaultEntityOperationPermissions()`: creates a generic set of permissions for every group content type. These can be overridden by @@ -139,8 +139,8 @@ Granting permissions to users ----------------------------- Similar to Drupal core, permissions are not directly assigned to users in -Organic Groups, but they are assigned to roles, and a user can have one or more -roles in a group. +OG, but they are assigned to roles, and a user can have one or more roles in a +group. The role data is stored in a config entity type named `OgRole`, and whenever a new group type is created, the following roles will automatically be created: @@ -148,8 +148,7 @@ new group type is created, the following roles will automatically be created: - `member` and `non-member`: these two roles are required for every group, they indicate whether or not a user is a member. - `administrator`: this role is not strictly required but is created by default - by Organic Groups because it is considered to be generally useful for most - groups. + by OG because it is considered to be generally useful for most groups. - Developers can choose to provide additional default roles by listening to the `og.default_role` event. @@ -178,8 +177,8 @@ methods: - `\Drupal\og\OgRoleManager::getDefaultRoles()`: creates the default roles and fires the event listener that modules can hook into the provide additional default roles. -- `\Drupal\og\EventSubscriber\OgEventSubscriber::provideDefaultRoles()`: Organic - Groups' own implementation of the event listener, which provides the default +- `\Drupal\og\EventSubscriber\OgEventSubscriber::provideDefaultRoles()`: OG's + own implementation of the event listener, which provides the default `administrator` role. @@ -191,16 +190,15 @@ of loading the `OgRole` entity and calling `$role->hasPermission()` on it, but this is not sufficient. There are a number of additional things to consider: - The super user (user ID 1) has full permissions. -- Organic Groups has a configuration option to allow full access to group - owners. +- OG has a configuration option to allow full access to group owners. - Users that have the global permission `administer organic groups` have all permissions in all groups. - The role can have the `is_admin` flag set which will grant all permissions. - Modules can alter permissions depending on their own requirements. -Organic Groups provides a service that will perform these checks. To determine -whether the currently logged in user has for example the `manage members` -permission on a given group entity: +OG provides a service that will perform these checks. To determine whether the +currently logged in user has for example the `manage members` permission on a +given group entity: ```php // Load some custom group. @@ -248,17 +246,17 @@ faster `::userAccess()` is not applicable. Checking if a user can perform an entity operation on group content ------------------------------------------------------------------- -Organic Groups extends Drupal core's entity access control system so checking -access on an entity operation is as simple as this: +OG extends Drupal core's entity access control system so checking access on an +entity operation is as simple as this: ```php // Check if the given user can edit the entity, which is a group content entity. $access_result = $group_content_entity->access('update', $user); ``` -Behind the scenes, Organic Groups implements `hook_access()` and delegates the -access check to the `OgAccess` service, so within the context of group content -this is equivalent to calling the following: +Behind the scenes, OG implements `hook_access()` and delegates the access check +to the `OgAccess` service, so within the context of group content this is +equivalent to calling the following: ```php /** @var \Drupal\og\OgAccessInterface $og_access */ @@ -285,8 +283,8 @@ Altering permissions -------------------- There are many use cases where permissions should be altered under some -circumstances to fulfill business requirements. Organic Groups offers ways for -modules to hook into the permission system and alter the access result. +circumstances to fulfill business requirements. OG offers ways for modules to +hook into the permission system and alter the access result. ### Alter group permissions @@ -326,9 +324,8 @@ function mymodule_og_user_access_alter(array &$permissions, CacheableMetadata $c ### Alter group content permissions -In addition to altering group level permissions, Organic Groups also allows to -alter access to group content entity operations, this time using an event -listener. +In addition to altering group level permissions, OG also allows to alter access +to group content entity operations, this time using an event listener. ```php