In part 1 of a series of four blog posts describing the transition from Monolith to a Decoupled System in Drupal 7, we explain the back end service and how we solved user roles and permissions.
Project Challenges
Hi, my name is George Baev and I am a Drupal Developer at Sangre. Right after joining the company I started working on a complex project. This project offered unique challenges to which we found some good solutions, which I would like to share with you.
The codebase in the project had a lot of existing functionality built in Drupal, which could not be removed or refactored at this point. Due to new UI requirements, we would have to separate the presentation layer outside of Drupal. In addition, new search features and the implementation of an AI component required some of the content to be looped and classified outside of Drupal. So we had to make a move towards a decoupled system.
The Move From Monolith to a Decoupled System
The main requirement was to transition from Drupal default content management to a decoupled system. There are numerous articles which explain the "Decoupled Drupal" such as How to decouple Drupal in 2018 and Decoupling Explained, but this summary describes decoupling very well (source from Decoupled Drupal):
Decoupled Drupal (or headless Drupal) allows the developer to utilize any technology to render the front-end experience in lieu of the theming and presentation layers in Drupal. This process utilizes Drupal as a content service. The Drupal back end exposes content to other systems, such as native applications, JavaScript applications, and IoT devices.
The project's Drupal 7 back-end maintains the basic data structures and provides the REST API end-points to allow the front-end to communicate with the back-end. The front-end React application allows on-place content editing, "drag and drop" functionality to design a Diagram layout and all basic data related operations which could be found in any Content Management System. This post will describe the Drupal 7 back-end part in more detail.
User Roles And Permissions Without The Drupal Roles And Permissions
One specific project requirement was to avoid using the Drupal built-in user roles and permissions system. The users should be group members and specific roles and permissions should be granted to these users in relation to the groups each user belongs to. This way, one Drupal user could have full permissions set granted to them in one Diagram, and a very limited permissions set granted to them in another Diagram.
There are group-related Drupal modules, the most well-known one being the Organic Groups module. We decided to use the Group Drupal contributed module. A paragraph from the Group module landing page compares the Group module to the Organic Groups module as follows:
Groups instead creates groups as entities, making them fully field-able, extensible and exportable. Every group can have users, roles and permissions attached to it. Groups can also act as a parent of any type of entity. Seeing as a group itself is also an entity, creating subgroups is very easy.
As seen from this explanation, the Group module fit in the project's requirements perfectly and allows future extensibility if needed.
The other requirement related to the Groups was that the number of users in a Group should be limited by predefined quota value. It should not be possible to add users to a Group if the Group quota limit was reached. Similar quota is set for the number of Diagrams in a Group. The quota related Group properties are described later in this post.
Back-end API
The front-end React application uses a REST back-end API to communicate with the back-end services. We chose the RESTful contributed module as a foundation for the API functionality. This module allows Drupal to be operated via RESTful HTTP requests, using best practices for security, performance, and usability.
All CRUD operations must be executed with the back-end API. These operations must be executed after data validations and according the current user's role and permissions. We shall explain the data validation and user authorization in the next post where the php traits will be described in more details.
The Groups and Group membership are managed through the API also. There are API end-points which grant or revoke the user's Group membership. Other API end-points allow users to be added to Groups or invited to Groups.
We modified the default response structure for many of the API end-points. We aggregated additional information with the RESTful module default response in order to reduce the number of front-end requests to the API.
Data Structures
Once we described the project challenges, we designed and implemented different data structures to store the Groups and Diagrams related data.
Groups
The Group Drupal contributed module comes with the "Default" Group type. We extended this Group type with the following custom fields to implement the quota-related limitations:
- Number of users. This is an integer field and contains the maximum number of users allowed in the Group.
- Diagrams allowed. This is an integer field and contains the maximum number of Diagrams allowed in the Group.
This setup guarantees that each newly created Group will contain the custom fields described above.
Group Roles
The Group module comes with several predefined user roles such as Anonymous, Outsider and Member. In addition, the Default Group type could include a predefined set of Roles which would be available for each Group created from the Default one. We defined the following Roles for all Group types:
- Owner. The Owner role is granted to the user who created the Group.
- Manager. The Manager roles does not allow the Group administration only; all other permissions are granted to this role.
- Editor. The Editors can create Diagrams and manage the Markers in the Diagrams; no other permissions are granted to them.
- User. No Group related permissions are granted to the Users.
Group User Permissions
This is the list of all predefined permissions (grouped in one hook_group_permission() function):
/** * Implements hook_group_permission(). */ function my_module_group_permission() { return [ 'can see all diagrams' => [ 'title' => t('Can see all diagrams'), ], 'can create diagrams' => [ 'title' => t('Can create diagrams'), ], 'can update any diagram' => [ 'title' => t('Can update any diagram'), 'restrict access' => TRUE, 'warning' => t('This permission allows deleting of Diagrams!'), ], 'can edit any diagram users' => [ 'title' => t('Can edit any diagram users'), ], 'can edit any diagram settings' => [ 'title' => t('Can edit any diagram settings'), ], 'can manage markers in diagrams' => [ 'title' => t('Can manage markers in the Diagrams'), ], ]; }
This set of permissions defines the major activities which are allowed the Group users. The permissions set could be easily extended to match future project requirements. The excellent Drupal administration interface allows defining each Group Roles permissions.
Users should be granted access to individual Diagrams in a Group. This requirement is solved with the Nodeaccess Drupal contributed module. The Nodeaccess module provides admin interface to grant the View, Edit and Delete permissions to a Diagram. The most appropriate place to check the user's node access is the hook_node_access() implementation as follows (some source code parts are removed):
/** * Implements hook_node_access(). */ function my_module_node_access($node, $op, $account) { if ($node->type != 'diagram') { return NODE_ACCESS_IGNORE; } $grants = _nodeaccess_get_grants($node); if (!empty($grants['uid'][$account->uid]['grant_' . $op])) { // The $op was explicitly granted to the account for this Diagram. return NODE_ACCESS_ALLOW; } }
Group Operations
The Drupal admin interface allows modification of the Group's title and predefined fields. This interface lists the Group members and allows changing the Group user Role as well as canceling the user's Group membership.
The Group members could be added by submitting the user's username. One interesting option is to invite users to the Group by sending an invitation email to each of the users. The following functionality describes how this members invitation functionality could be implemented.
The POST request data should be submitted in the following format:
[ { "email": "[email protected]", "role": "user" }, { "email": "[email protected]", "role": "editor" }, { "email": "[email protected]", "role": "user" } ]
The validated request is processed as follows:
$gid = array_pop(explode('/', $request->getPath())); $body = $request->getParsedBody(); if (empty($body)) { throw new BadRequestException('No user data submitted!'); } $group = $this->checkGroupPermission($gid, 'administer members', 'users', $body); foreach ($body as $value) { $user = user_load_by_mail($value['email']); if (!is_object($user)) { // Create the user. $new_user = [ 'name' => $value['email'], 'mail' => $value['email'], 'pass' => user_password(), 'status' => 1, 'access' => REQUEST_TIME, ]; $user = user_save(NULL, $new_user); } if (is_object($user)) { if ($group->getMember($user->uid)) { // The user is a member of the Group so don't add him. continue; } // Add the user to the group. $group->addMember($user->uid); $membership = group_membership_load($gid, $user->uid); $membership->grantRoles([$value['role']]); } }
The Group id is included in the request's URL, for example http://myapp.docker.localhost:8000/api/v1.0/groupadduser/2 . This example request will process the submitted users' data for the Group with ID=2.
The functionality described above is extracted from the corresponding GroupAddUser REST API end-point. We'll discuss the REST API implementation in more details in next posts.
User and Diagram Quotas
As we mentioned in the beginning, there are predefined integer fields for the Users and Diagrams quota in each Group. The access based on the quota limits is controlled in the REST API. It is easy to automatically "attach" the number of current users and Diagrams in a Group as follows:
/** * Implements hook_entity_load(). */ function my_module_entity_load($entities, $type) { if ($type == 'group') { foreach ($entities as $entity) { // Get the number of users in the group. $entity->my_users_used = count($entity->getMembers()); // Get the number of Diagrams in the group. $entity->my_diagrams_used = count($entity->getEntitiesOfType('node', 'diagram')); } } }
The my_users_used and my_diagrams_used properties are automatically available when the Group entity is loaded. This way, if each of these properties' value equals the number of allowed users or Diagrams, no user or Diagram could be added to the Group.
In my next post, I will continue to explain the Diagrams, Markers and Paragraphs as the basic content types of the system. I will describe the connections among Time Ranges, Sectors and Markers in the Diagram and the other unique challenges they offered. See you soon!