The illustration shows Site with Drupal logo guarded securely hidden behind a net

Drupal node grants

Everyone who does Drupal development will sooner or later encounter the need to define tighter control of access to content. The standard mechanisms of roles and permissions are very flexible, but they may be insufficient in complex projects. When access to nodes starts to depend on, for example, fields assigned to a given user, then you have to take advantage of more advanced solutions. In Drupal 7 and 8 we can use a hook – hook_node_access() or a so-called grants mechanism.

Permission control using hook_node_access()

In cases where advanced permission control is needed, hook_node_access() is the most popular choice. There are some slight differences between Drupal 7 and 8, but the way it works is more or less the same. It takes three arguments: $node, $op and $account.

With every attempt to access a given node, Drupal will call mymodule_node_access() in all modules and check whether a given user $account has all the necessary permissions to run $op in a $node. The hook should return one of the following responses:

return AccessResult::allowed(); // (or NODE_ACCESS_ALLOW in version 7)
return AccessResult::forbidden(); // (or NODE_ACCESS_DENY in version 7)
return AccessResult::neutral(); // (or NODE_ACCESS_IGNORE in version 7)

Simple, yet effective, isn’t it? This is what it looks like in practice (D8):

use Drupal\Core\Access\AccessResult;
use Drupal\node\NodeInterface;
use Drupal\Core\Session\AccountInterface;

function mymodule_node_access(NodeInterface $node, $op, AccountInterface $account) {
  $type = $node->getType();
  if ($type == 'foo' && $op == 'view') {
    if(strstr($account->getEmail(), '@example.com')) {
        return AccessResult::allowed();
    }
    else {
      return  AccessResult::forbidden();
    }
  }
  return AccessResult::neutral();
}

It would seem like access control using hook_node_access() would be enough for even the most advanced needs, but as they say, there’s no rose without thorns. Checking a long list of nodes one by one will surely negatively impact efficiency. In particular, the Views and Menu modules cannot afford to use that hook too often. Due to optimisation reasons, the developers of Drupal limited the calling of hook_node_access() only to nodes displayed in the “Full” mode. In all other cases, you have to rely on standard roles and permission system or use less resource-heavy Access Grants.

Access Grants

Even though it’s very easy to understand the principle of operation of hook_node_access(), Access Grants is a far more complex subject. Some bloggers try to present it using a metaphor for doors, locks and keys. Personally, I think that stretches the truth a little bit too much. That’s why I’m going to go with a comparison to a secret military base.

Attention! What does a military base have in common with Drupal? Well, for instance, both of them have restricted areas where you can get only if you have proper authorisations and permissions. What is more important, in both cases the users and visitors are given passes that clearly define the scope of what they can see or do upon entry.

In standard permission system used by Drupal, we have a single guard, who checks the role of a user on their pass. The role defines all the actions that the user can do (e.g. reading a Type A node, reading and writing to a B type node...) 

In mymodule_node_access(), there are multiple guards, one for each module. Each one of them identifies the user and determines if they will be given access based on selected criteria. Every guard has full access to user data, as well as to the protected area (node) – their decision is based on all of these factors.

What is the role of the aforementioned Access Grants? They divide the authorisation process into two separate parts, which are served by two separate hooks. The way they are used in Drupal 7 and 8 is almost identical.

1) hook_node_access_records()

The first hook is activated when the node is saved. It has one argument – $node. The role of a hook is to determine the list of the so-called grants – passes that have to be showed when access to the node is attempted. 

function mymodule_node_access_records(\Drupal\node\NodeInterface $node) {
  $grants = [
    [
      'realm' => 'realm1',
      'gid' => 12345,
      'grant_view' => 1,
      'grant_update' => 1,
      'grant_delete' => 1,
      'priority' => 0,
    ],
  ];
  return $grants;
}

Every grant has its own realm. The user, who attempts to access a given node has to have at least one grant (pass) in every defined realm. For example, if Drupal builds the following list of grants based on all the hooks it finds:

$grants[] = 
[ 'realm' => 'realm1', 'gid' => 123, … ],
[ 'realm' => 'realm1', 'gid' => 456, … ],
[ 'realm' => 'realm2', 'gid' => 789, … ];

The user needs to have at least one realm1 pass and one realm2 pass in order to access the node.

Complicated? I’ll try to present it using my military analogy. Let’s assume that our secret base has several departments (we’ll use them to convey the concept of realms). Every department has its own guard at the entrance to every restricted area. A user who wants to access a given area needs to go to each guard and shows that he has an appropriate pass (that is, a grant). If the guard determines that user’s pass complies with the requirements of his department, he allows the user to pass further. If the user doesn’t have a pass, they won’t be able to access the area and he won’t be checked by other guards.

Why is this approach faster? It’s because the guard doesn’t have to know the details of authorisation (or have access to the node at all), he only needs to compare the passes.

What is a realm?

A realm (or as I called it in my military metaphor – a department) can be any string, usually, the name of a module is used. Such an approach makes the grants system somehow cleaner and more understandable – the user needs to have one grant (pass) for each module that defines its own node access rules using hook_node_access_records(). You can use this convention if you don’t consider it to be overly simplified. 

All or nothing

Using authorisation mechanisms, sometimes the need arises to use two extreme scenarios: access for all or only for the administrator. In the first case, all that is needed is to return an empty list of grants in a hook. This will mean that in a given realm, the users don't need any passes. In the second case, one has to define a grant that won’t be given to any user whatsoever.

2) hook_node_grants()

Who grants the grants (passes) to the users? The answer is – again – a hook. In this case, it’s hook_node_grants(), which is executed at each attempt to access a given node. It uses two arguments – user account $account and the operation – $op. Using these two arguments, it determines the list of grants identified using realm (key) and identifier (value).

use Drupal\Core\Session\AccountInterface;

function mymodule_node_grants(AccountInterface $account, $op) {
  if ($op == 'view') {
    $grants['realm1'] = array(123);
    return $grants;
  }
}

Notice, that the above hook doesn’t have access to $node. This is why the grants mechanism is much quicker, as it doesn’t require the node to be loaded each time it needs to be accessed. Thanks to that grants defined by you can be taken into consideration during building views and generating menus.

Permission reset

When you start working with the grants mechanism, you can stumble upon several unexpected problems. When your authorisation doesn’t work properly, you should first and foremost try and rebuild the node_access table in Drupal’s database. You can do it using node_access_rebuild() function or by going to /admin/reports/status/rebuild (you can find this link in the Status Report in your administration panel).

If your grants won’t work as intended after a rebuild, give the node_access table a good read and check if it contains a row with nid=0, gid=0, realm=all. This entry is added by default during Drupal installation and it turns off the entire grants system. This row doesn’t indicate any error – without that entry, the access to content on the entire website would be available to the administrator only.

Types of operations

Most probably you noticed that I omitted the types of operations on a node in my description of grants. You can decide what types of operations (view/update/delete) will be available to each user in both hooks presented above (just check the examples!) You have to remember one thing: if a number of grants will be available for a given realm, the one with the highest access level will be always selected.

Summary

At the end of this article, I want to remind you that all the permission mechanisms in Drupal work in parallel. You can, therefore, mix all the authorisations and access control methods together. It’s also worth noting that there’s an ongoing discussion at drupal.org to turn the grants system into something that’s easier to understand and more in line with an object-oriented approach. However, we wouldn’t expect any revolutionary changes anytime soon.

Dismissed! :-)

3. Best practices for software development teams