Custom Permissions with Node Access Grants in Drupal 8 and Drupal 7
For many Drupal web sites setting permissions for anonymous, authenticated, and admin users through the GUI is sufficient. For example, all published content should be visible to all users, authenticated users can leave comments, and admin users are allowed to create content. For more advanced use cases the popular contributed module Content Access (beta for Drupal 7, dev for Drupal 8) allows much finer grained control over read and write access to nodes by content type, and can even specify access differently for individual nodes.
When even more complex permissions are needed many choose to implement hook_node_access(). Permissions management with hook_node_access() does have a few disadvantages:
- Unwieldy implementations can cause considerable performance bottlenecks
- Node operation links like View or Edit associated with node permissions aren’t automatically added or removed
- Views queries are unaffected; content could be displayed to a user in a views block which they would otherwise not have access to
Managing permissions with hook_node_access() works fine in many cases, but it’s not the most flexible way to manage access to your nodes.
Custom permissions with node access grants in Drupal 8
A more robust solution to complex permissions is to use the node access system with hook_node_access_records() and hook_node_grants(). Hook_node_access_records() is called when each node is saved. That’s where grants are setup to view, update, and/or delete a node. Hook_node_grants() is called to determine access and is what is used to check the node_access table.
The good news is node access grants work (almost) exactly the same in Drupal 8 as in 7.
When researching how to implement node grants, I had come across relatively simple examples where access was based on a user’s role or organic groups properties. Since the user object is passed to hook_node_grants(), it’s trivial to determine which user should get access. But, what if access to view or edit a node is based on a combination of factors? This was the situation I recently had to deal with.
The implementation below creates a View grant for accounts that meet a specific criteria. The code for the actual criteria has been omitted. It also creates a full access grant for administrators using a zero as the grant id -- not to be confused with the UID associated with anonymous users.
function MODULENAME_node_access_records(NodeInterface $node) {
// code to get accounts that should have read access is not shown
foreach ($accounts as $account) {
$grants[] = array(
'realm' => 'custom_access',
'gid' => $account->id(),
'grant_view' => 1,
'grant_update' => 0,
'grant_delete' => 0,
'langcode' => 'en',
);
}
$grants[] = array(
'realm' => 'custom_access',
'gid' => 0, // This is the admin GID, not the $account->uid associated with anonymous
'grant_view' => 1,
'grant_update' => 1,
'grant_delete' => 1,
'langcode' => 'en',
);
return $grants;
}
Above is part of a hook_node_access_records() implementation. The node_access tables store:
- Node id: The unique node identifier.
- Realm: A string that can be whatever you want. This can be useful to group different kinds of access; using the modulename is typical.
- Grant id: An integer value often used to group access. If for example some users can only read the node, and others can read, update, and delete, you might use 0 and 1 for these two sets of users. In our case there are a small number of users who should have read access and this is determined by code based on multiple factors. For this reason we set a grant for each user using the user id.
- Grant_view, grant_update, grant_delete: Use 0 for no access, 1 for access.
- Langcode: Language code.
Below is the hook_node_grants() implementation. This is called each time access to a node needs to be determined; so the simpler the code, the better. If the node_access table has an entry for the node id being accessed, permissions with the matching value for realm and grant id will be granted. First the account is checked for the administrator role, and the grant id 0 is returned if there’s a match. If not, and if the user isn’t anonymous, the function returns a grant with the user’s id. If there’s a match in the table, access will be granted based on the values for read, update, or delete. If this grant doesn’t match an entry in the table, access will be denied. Finally, if the user is anonymous an empty array will be returned, denying access.
function MODULENAME_node_grants(AccountInterface $account, $op) {
$grants = array();
if (in_array('administrator', $account->getRoles())) {
// gid to view, update, delete
$grants['custom_access'][] = 0;
return $grants;
}
if ($account->id() != 0) {
// otherwise return uid, might match entry in table
$grants['custom_access'][] = $account->id();
}
return $grants;
}
Implications of custom node access grants
One of the limitations of implementing custom node access grants is the effect on database queries. If the current user does not have access to a particular node it won’t be included in query results. This makes sense for Views since you wouldn’t want to display nodes a user shouldn’t have access to. However, if in code you need to query nodes in the background, the query is limited to those the current user can access. If for some reason a view should ignore access checks, that’s configurable with the "Disable SQL rewriting" option in the Views GUI.
For queries in code, starting in Drupal 7.15 the syntax for disabling access checks while performing a query is below:
$query->addTag('DANGEROUS_ACCESS_CHECK_OPT_OUT')
in Drupal 8 the same thing is accomplished with:
$query->accessCheck(FALSE);
Using node access grants isn’t always necessary to manage your permissions, but it allows for more complexity than many contributed modules and is more efficient than many custom hook_node_access() implementations.