Writing a module that handles node access
Using Drupal’s built-in node grants and realm access system, you can control which users or user roles can perform different operations such as view, update, and delete on a per node basis. You can apply similar access control to non-node entities via access hooks.
This page provides an example that limits viewing of particular nodes to authenticated users. It should provide an outline and illustrate how to write your own access module. Alternatively you may want to watch Drupal Camp presentation of node access (Youtube).
Let's look first at the node_access table. By default, it looks like this:
nid | gid | realm | grant_view | grant_update | grant_delete
-----+-----+-------+------------+--------------+--------------
0 | 0 | all | 1 | 0 | 0
This single row determines that all nodes can be viewed but not updated or deleted. If you use a module that implements node access, this row is usually replaced with one or more rows per node.
If the node_access table becomes out of sync with what is defined in code, you may experience nodes that are unexpectedly hidden or visible. If that happens, or if you change the modules that implement node access, use the admin/reports/status page to rebuild it.
There are two hooks you'll want to implement: hook_node_grants() and hook_node_access_records().
Custom code
We'll start by defining a realm and two grant IDs. Ignore the realm, for now, we'll just use the module name. A grant ID is an arbitrary integer that groups content together in some way. It's often the same as a role ID or taxonomy term, but it doesn't have to be. In this example, we'll define two grant IDs, one for public content and one for private content.
define('MYMODULE_REALM', 'mymodule');
define('MYMODULE_GRANT_ID_PUBLIC', 0);
define('MYMODULE_GRANT_ID_PRIVATE', 1);
In our example, we'll consider node 2 to be private, and all other nodes to be public. We'll allow all users to access public content, and only those with the authenticated user role to access private content.
hook_node_grants()
The hook_node_grants() function is responsible for giving out grants for a given user and operation (view/update/delete). We will return grants for the view operation. We ignore other operations and leave those decisions to another module.
All we do here is examine the user as specified in $account, and provide an array of grant IDs for our realm.
/**
* Implementation of hook_node_grants().
*/
function mymodule_node_grants($account, $op) {
// we're only interested in providing rules for viewing content,
// update and delete can be handled elsewhere by the content module
// and it's permissions
if ($op == 'view') {
if (array_key_exists(DRUPAL_AUTHENTICATED_RID, $account->roles)) {
// this is an authenticated user, give them both the 'public' and
// 'private' grant IDs to allow them access to everything
$grants[MYMODULE_REALM] = [
MYMODULE_GRANT_ID_PUBLIC,
MYMODULE_GRANT_ID_PRIVATE,
];
}
else {
// this is an anonymous user, give them the 'public' grant ID
// that allows them access to public nodes
$grants[MYMODULE_REALM] = [
MYMODULE_GRANT_ID_PUBLIC,
];
}
return $grants;
}
}
hook_node_access_records()
The hook_node_access_records() function is responsible for populating the node_access table. It does this whenever a node is saved, or if the node permissions are rebuilt.
How this works:
- First, we examine the node to see if it is private or public.
- Secondly, we return grants accordingly. We could return multiple grants, but we don't need to because our hook_node_grants will return multiple grant IDs if appropriate.
/**
* Implementation of hook_node_access_records().
*/
function mymodule_node_access_records($node) {
// use $node to make a decision as to which grants to make.
// this is a trivial example based on the node ID but illustrates
// where you would put more meaningful business logic.
if ($node->nid == 2) {
$private = TRUE;
}
else {
$private = FALSE;
}
if ($private) {
// this is a private node, so add a rule that allows it to be viewed
// by those with the 'private' grant ID
$grants[] = [
'realm' => MYMODULE_REALM,
'gid' => MYMODULE_GRANT_ID_PRIVATE,
'grant_view' => 1,
'grant_update' => 0,
'grant_delete' => 0,
'priority' => 0,
];
}
else {
// this is not a private node, so add a rule that allows the
// 'anonymous user' grant ID view access. We'll assume that
// mymodule_node_grants() gives authenticated users both
// 'authenticated user' and 'anonymous user' grant IDs, so there
// is no need for more than one rule here.
$grants[] = [
'realm' => MYMODULE_REALM,
'gid' => MYMODULE_GRANT_ID_PUBLIC,
'grant_view' => 1,
'grant_update' => 0,
'grant_delete' => 0,
'priority' => 0,
];
}
return $grants;
}
Now let's rebuild the permissions and have a look at the node_access table:
nid | gid | realm | grant_view | grant_update | grant_delete
-----+-----+----------+------------+--------------+--------------
1 | 0 | mymodule | 1 | 0 | 0
2 | 1 | mymodule | 1 | 0 | 0
The important thing to note here is that authenticated users will have both grant IDs 0 and 1. Therefore they will see both node 1 and node 2. Anonymous users will only have grant ID 0, so will only see node 1.
This does what we want. Log in and you should see nodes 1 and 2. Log out and you should only see node 1.
Unpublished nodes
Once your module implements hook_node_grants(), Drupal removes its default fallback row (nid=0, realm=all) from the node_access table and hands all access control to your module.
There is a consequence to this: your module must now explicitly account for unpublished nodes, because the core permission "view own unpublished content" and Content Moderation permission "view any unpublished content" are enforced at the query layer through grant rows, not through the permission system alone.
If you do not add grant rows for unpublished nodes, they will disappear from listings and Views for all users except those with "bypass node access", even if those users have the appropriate unpublished content permissions.
The solution is to define two additional realms in hook_node_access_records() that cover the two unpublished permissions, and issue the corresponding keys from hook_node_grants().
/**
* Implementation of hook_node_access_records().
*/
function mymodule_node_access_records($node) {
$grants = [];
// ... your existing published node logic here ...
// For unpublished nodes, add grant rows that mirror the two core
// permissions for viewing unpublished content. Without these rows,
// unpublished nodes will not appear in listings or Views for any
// user, regardless of their permissions.
if (!$node->isPublished()) {
// Grant view access to users who can view any unpublished content,
// or who have bypass node access. A fixed gid of 1 is used here;
// the matching key is issued in hook_node_grants() below.
$grants[] = [
'realm' => MYMODULE_REALM_UNPUBLISHED_ALL,
'gid' => 1,
'grant_view' => 1,
'grant_update' => 0,
'grant_delete' => 0,
'priority' => 0,
];
// Grant view access to users who can view only their own unpublished
// content. The gid is the node owner's user ID so that the grant
// matches only that specific user's key in hook_node_grants().
$grants[] = [
'realm' => MYMODULE_REALM_UNPUBLISHED_OWN,
'gid' => $node->getOwnerId(),
'grant_view' => 1,
'grant_update' => 0,
'grant_delete' => 0,
'priority' => 0,
];
}
return $grants;
}In hook_node_grants(), issue the corresponding keys based on the user's permissions:
/**
* Implementation of hook_node_grants().
*/
function mymodule_node_grants($account, $op) {
$grants = [];
if ($op == 'view') {
// ... your existing grants here ...
// Issue a key for the 'any unpublished' realm to users who can view
// all unpublished content or who can bypass node access entirely.
if ($account->hasPermission('bypass node access') ||
$account->hasPermission('view any unpublished content')) {
$grants[MYMODULE_REALM_UNPUBLISHED_ALL] = [1];
}
// Issue a key for the 'own unpublished' realm using the user's ID.
// This matches the gid stored in hook_node_access_records() for
// nodes owned by that user.
if ($account->hasPermission('view own unpublished content')) {
$grants[MYMODULE_REALM_UNPUBLISHED_OWN] = [$account->id()];
}
}
return $grants;
}After adding this code and rebuilding permissions, the node_access table will contain rows for unpublished nodes alongside your existing rows:
nid | gid | realm | grant_view | grant_update | grant_delete
-----+-----+------------------------------+------------+--------------+--------------
1 | 0 | mymodule | 1 | 0 | 0
2 | 1 | mymodule | 1 | 0 | 0
3 | 1 | mymodule_unpublished_all | 1 | 0 | 0
3 | 42 | mymodule_unpublished_own | 1 | 0 | 0
In this example, node 3 is unpublished and owned by user 42. Users with "bypass node access" or "view any unpublished content" will hold the key gid=1 in the mymodule_unpublished_all realm and will see it in listings. User 42 will hold the key gid=42 in the mymodule_unpublished_own realm and will also see it. All other users will see neither row and the node will be correctly hidden from them.
Note that "view any unpublished content" is provided by the Content Moderation module rather than node core. If your site uses Content Moderation you should check for this permission in hook_node_grants() as shown above. If it does not, you can omit that check, though including it does no harm.
Help improve this page
You can:
- Log in, click Edit, and edit this page
- Log in, click Discuss, update the Page status value, and suggest an improvement
- Log in and create a Documentation issue with your suggestion