Khanh Hoang - Kenn
Kenn is a user experience designer and front end developer who enjoys creating beautiful and usable web and mobile experiences.
Today, I will talk a bit about access control. Specifically, what options you as a module developer have for controlling access to nodes and individual fields. As I'm sure you agree, the Drupal permission system available for site builders is powerful. Yet, fringe cases occur that leave this system wanting. It's a case like this I will talk about below.
The Drupal core permissions system for site builders is very flexible. You can control what kinds of access user roles have over the content (nodes). This means that you can grant users of a particular role the following permissions (to):
And these permissions can be further targeted towards specific content types. So for instance you can have users of a certain role create Article nodes but not Basic pages.
With these permissions you have a lot of ground covered. But there are cases in which you'll need to write some code to account for what you need. Let's look at one of these cases, albeit simplified for this article.
Users of the roles Supervisor can create articles. They can then edit all Articles that other Supervisors created but not the ones they themselves created. And herein lies the problem. With the Drupal permissions page alone, you just can't get there.
If Supervisors can edit all Article nodes, that includes also the ones they created. So how to tackle this problem? Using hook_node_access() in your custom module.
So what can you do with this hook? The short version is that you can control access to nodes.
This hook gets called by the node access system to determine if the user that wants to see the node being loaded, should. Using this hook, the system checks if there are any modules that actively want to grant or block access to the node in question. Let's see how we can implement this to solve our problem:
/** * Implements hook_node_access(). */ function module_name_node_access($node, $op, $account) { // If Supervisor, do not allow to edit their own node. if ($account->roles[3] && $account->uid == $node->uid && $node->type == 'article') { if ($op == 'update') { return NODE_ACCESS_DENY; } } }
Let's do a quick rundown of what happens in this code. The function takes three parameters: the $node object being loaded, the $op (operation) being performed and the user $account object trying to access. All the right ingredients basically.
First, we perform three checks in an if conditional. We check if the current user has theSupervisor role. We know that this role has an id of 3. Then we check if the id of the current user equals the one of the author of the node being loaded. Finally, we check if the node being loaded is an Article.
If these three checks pass, we move on to the operation. There can be 4 different types of operations: create, update, delete and view. We check if the operation being performed is update, and if it is, we return the NODE_ACCESS_DENY constant to deny access to the user. Not so complicated.
This will therefore deny access to the node author if s/he wants to edit it. However, this will not stop him or her from viewing the node or even deleting it. But you can perform logic that will help you control that as well if you want. And even more complex stuff like access depending on relationships between users. But again, story for another day.
To illustrate a slightly more complex case, imagine that our Supervisor should be allowed to edit his or her Article, but not all fields on that node. Field X must be visible but impossible to update. However, the Supervisor must be able to edit Field X on all the other Articles.
In this case, our implementation of hook_node_access() won't do the job since it controls access to the entire node. We'll need a more granular approach, provided by, you guessed it, hook_field_access().
Before explaining how this works, I want to point out the existence of the great Field Permissions module. Using this module, we bring the power of the core Drupal node permissions down to the level of fields. Which is awesome. However, we are restricted in our case by the same implication of granting a role the permission to edit all content of a certain type. So let's see what hook_field_access() brings to the table.
The function works similarly to the previous one, but it takes different parameters. You have again operation ($op) which only includes view and edit in this case. Then you have the $field on which the operation is performed. The third one is the $entity_typethe field is attached to, which means using this hook is not restricted to nodes only. Then you have the two optional ones, the $entity object itself and the user $accountthat tries to access.
And yet again let's see some code that will help us with our slightly more complex case:
/** * Implements hook_field_access(). */ function module_name_field_access($op, $field, $entity_type, $entity, $account) { // If Supervisor, do not allow to edit Field X on their own Article if ($account->roles[3] && $account->uid == $entity->uid && $entity_type == 'node' && $entity->type == 'article' && $field['field_name'] == 'field_x') { if ($op == 'edit') { return false; } } }
In this function you'll notice a similar logic but oriented towards the field. We check if the accessing user is a Supervisor, if they are the author of the entity, if the entity type is a node, if the node type is an Article and if the field requested is named field_x. If all these are true, we move on to check the operation and return false (no constants here) to deny access to this user from editing the field (if $op == 'edit').
So what happens in practice? The user can view the Article s/he created. But if s/he edits the Article, Field X is hidden. If the user then goes to edit the Article created by another user, s/he can edit Field X. Neat.
One thing to note here is that since hook_field_access() has only 2 types of operations, if you check against edit, you'll deny access to the user even when first creating the node. To circumvent this behavior, you can perform an additional check. In addition to($op == 'edit'), you can check also if the $entity in question has an id set. Since we are talking about nodes here, you can replace this line:
if ($op == 'edit') {
With this:
if ($op == 'edit' && isset($entity->nid) {
This way, if the node is not yet created, the user can work with Field X. But as soon as the node has been saved, the values of Field X can no longer be changed by this user. But this of course depends on your needs.
To recap, we've seen above how to control access to nodes and fields in a custom module. All you have to do is implement these hooks (as needed) and perform your logic inside. One thing I urge you though is to always document properly this kind of code to make it easier to understand later why certain users have what can seeminexplicable permissions.
Another interesting thing we've seen is that even one of the most powerful features Drupal has cannot account for all real life cases. But not to worry, under the hood, Drupal allows us to customize its behaviour with just a few lines of code to meet our exact requirements. And be honest, was that complicated?