Drupal 7 Node Access Control With Access Grants

20th July 2015

There are a few ways in which you can create complex node access systems. Modules like Taxonomy Access Control and Node Access will allow you to restrict node access in different ways, and work very well for setting up taxonomy or role based access control. There are a few edge cases where you need to restrict access to a node based on some arbitrary conditions like the age of the user or the contents of a field. This is where the build in Drupal access control mechanisms come into play. They do take a little bit of effort to get around how they work, but I hope to enlighten in this post.

There are two hooks that work together in order to facilitate these node level access permissions. These hooks are hook_node_grants() and hook_node_access_records(). Together they create a sort of lock and key system where users who have the right key can see content with the right lock. These hooks are built into Drupal core and are available without having to install any extra modules (other than the one you use to implement the hooks that is).

The good thing about these hooks is that they are understood by many other modules and as such any permissions that are applied to nodes are implemented across the entire site. This means that you don't need to do anything to get the permissions included into modules like Views as this functionality is baked in.

Lock Creation

The first step in all of this is to setup your locks. The hook_node_access_records() hook is used by Drupal when saving a node to see what sort of access rights are needed. It is also fired when updating a nodes permissions. All this function needs to do is return an array that tells Drupal what the lock should look like. This array needs to contain the following items:

  • nid : The ID of the node.
  • realm : The realm of the grants, which would normally be the module name.
  • gid : The grant ID, which is really a way of matching the user with the access (we'll come onto that bit later). This must be a numeric value, but it can be any value that you want.
  • grant_view : A numeric value to determine the access of a user to view the node, with 1 meaning that the user is part of this group and should be granted access. This should be set to the status of the node ($node->status) as there is a danger that unpublished nodes can be viewed.
  • grant_update : A numeric value to determine the access of a user to update the node, with 1 meaning that the user is granted access.
  • grant_delete : A numeric value to determine the access of a user to delete the node, with 1 meaning that the user is granted access.
  • priority : The priority of the grant. This comes into play when multiple modules are battling against each other to grant or deny access to a node. The higher the priority level wins out in this instance.

The official Drupal guidelines state to leave the priority at 0, but I think setting it to 1 is probably a better approach. This ensures that your own permissions trump any normal Drupal restrictions. Be aware though that your own permissions will trump absolutely everything so you need to make sure that you take into account things like published status if you still want those. It's also important to remember that if the hook is called and a blank array is returned then that item of content will get the default access restrictions.

After calling this hook Drupal will write the data into a table called node_access. This table will contain the contents of the array items that the hook returned.

Rather than just present this hook in it's own it's best to show the hook being called with some context. Lets say that we have a content type with a field called 'field_age_restriction' that is a simple boolean checkbox. We can use this checkbox as a way to restrict access to a node based on the age of the user, so we use the hook_node_access_records() hook to generate the access rights that the user needs to meet before they can see the content. What we do in this hook is make sure we have the correct content type, extract the field value from the node, and then setup the access array based on the value of that field.

  1. /**
  2.  * Implements hook_node_access_records().
  3.  */
  4. function mymodule_node_access_records($node) {
  5. $grants = array();
  6.  
  7. // Make sure we have the correct content type.
  8. if ($node->type == 'article') {
  9. // Extract the value of the 'field_age_restriction' field.
  10. $content_age_restriction = field_get_items('node', $node, 'field_age_restriction', 'und');
  11. if ($content_age_restriction !== FALSE) {
  12. $content_age_restriction = array_pop($content_age_restriction);
  13. if ($content_age_restriction['value'] == 1) {
  14. // If we have an age restricted node then setup the grant.
  15. $grants[] = array(
  16. 'nid' => $node->nid,
  17. 'realm' => 'mymodule_age_restriction',
  18. 'gid' => 1,
  19. 'grant_view' => $node->status,
  20. 'grant_update' => 0,
  21. 'grant_delete' => 0,
  22. 'priority' => 1
  23. );
  24. }
  25. }
  26. }
  27.  
  28. return $grants;
  29. }

Assuming that a node has been published and saved with age restriction turned on we will have a lock that looks like this.

  1. node = 123
  2. realm = mymodule_age_restriction
  3. gid = 1
  4. view = true
  5. update = false
  6. delete = false

Key Creation

Now that we have setup the lock we need to give each user a key to that lock. We do this using a hook called hook_node_grants(), which takes two parameters.

  • account : The current user account object.
  • op : The current action being performed on the content item. This will be 'view', 'update', or 'delete'.

The hook should return a list of grants that the user has access to, which should equate to the realm and gid's that were setup in the hook_node_access_records() hook. The observant reader might immediately see that there is nothing in this hook that references the original node (there is no $node parameter for this hook). This is intended, and is perhaps the most difficult thing to understand about the process involved here. The lock and key analogy fits in this regard as there is nothing in the lock or the key that has any knowledge of the other, it's just that the key has a structure that fits the lock. The key that the user is given is the array returned from this hook, which should fit the structure of the lock as defined in hook_node_access_records(). The return value needs to have a key that matches the realm and an array of gids that the user is a member of.

Building upon the previous example we can build a key that allows the user to view age restricted content. We will give users a field which is a boolean value that means that they are over 18. We could implement a date field to do this based on the users date of birth, but for demonstration purposes this boolean value is simpler to understand. The following is an implementation of the hook_node_grants() hook.

  1. /**
  2.  * Implements hook_node_grants().
  3.  */
  4. function mymodule_node_grants($account, $op) {
  5.  
  6. $grants = array();
  7.  
  8. // Set default grants condition.
  9. $grants['mymodule_age_restriction'] = array(0);
  10.  
  11. // Tackle the 'view' operation.
  12. if ($op == 'view') {
  13. // Make sure the user is logged in.
  14. if (user_is_logged_in() !== FALSE) {
  15. // Extract the value of the 'field_user_is_18' field.
  16. $current_user = user_load($account->uid);
  17. $user_is_18 = field_get_items('user', $current_user, 'field_user_is_18', 'und');
  18. if ($user_is_18 !== FALSE) {
  19. $user_is_18 = array_pop($user_is_18);
  20. if ($user_is_18['value'] == 1) {
  21. // User has correct value, allow them access with a gid of '1'.
  22. $grants['mymodule_age_restriction'] = array(1);
  23. }
  24. }
  25. }
  26. }
  27.  
  28. return $grants;
  29. }

What we return here is an array of realms containing the gids that the user is allowed to be part of. So in the above example the realms is 'mymodule_age_restriction' and the gid of 1 matches the gid that we setup in the hook_node_access_records() hook. So, if we assume that the user has this boolean field for age restriction checked we will have a key for that user which looks like this.

  1. realm = mymodule_age_restriction
  2. gid = 1
  3. view = true

When applied to the lock it can be seen that this allows the user to view the content based on the matching realm, gid, and actions fields (which would be view, update, or delete). This also prevents any special access to editing and deleting the node. If any permission is left out then the permission defaults back to the normal Drupal permissions, which in the case of editing and deleting would be false. If any module allows access to a node then the access is granted, there is no way to 'unset' the permission from a different hook.

This hook is only ever called for users who require a permission, and the Drupal superuser (i.e. user number 1) will always bypass these access checks. In addition to this, if the user has been given the 'bypass node access' permission then they will also bypass these access checks.

Implementing More Complicated Conditions

Because the return value of the hook_node_grants() hook is an array of gids we can return more than one value that might match different gids as defined by the hook_node_access_records() hook. Taking this a step further we can show how this can be utilized by essentially recreating the functionality of Taxonomy Access Control and matching users with nodes using the taxonomy term ID as the gid.

This is an implementation of the hook_node_access_records() hook, which creates an access lock that contains all of the taxonomy terms contained in the field field_tags.

  1. /**
  2.  * Implements hook_node_access_records().
  3.  */
  4. function mymodule_node_access_records($node) {
  5. $grants = array();
  6.  
  7. // Make sure we have the correct content type.
  8. if ($node->type == 'article') {
  9. // Extract the value of the 'field_tags' field.
  10. $content_tags_restriction = field_get_items('node', $node, 'field_tags', 'und');
  11. if ($content_tags_restriction !== FALSE) {
  12. // Because we may have nodes with duplicate terms we need to create a unique array or terms.
  13. $tags = array();
  14. foreach ($content_tags_restriction as $tag) {
  15. $tags[] = $tag['tid'];
  16. }
  17. $tags = array_unique($tags);
  18. // Apply the unique terms to the access record.
  19. foreach ($tags as $tag){
  20. $grants[] = array(
  21. 'nid' => $node->nid,
  22. 'realm' => 'mymodule_tags_restriction',
  23. 'gid' => $tag,
  24. 'grant_view' => 1,
  25. 'grant_update' => 0,
  26. 'grant_delete' => 0,
  27. 'priority' => 1
  28. );
  29. }
  30. }
  31. }
  32.  
  33. return $grants;
  34. }

Assuming that a node is given three taxonomy terms in the 'field_tags' a lock that looks like the following table is generated.

noderealmgidviewupdatedelete
139mymodule_tags_restriction7100
139mymodule_tags_restriction8100
139mymodule_tags_restriction9100

We then adapt the hook_node_grants() hook to use the taxonomy array in the same way. We are using the same field that is used on the node here.

  1. /**
  2.   * Implements hook_node_grants().
  3.   */
  4. function mymodule_node_grants($account, $op) {
  5.  
  6. $grants = array();
  7.  
  8. // Set default grants condition.
  9. $grants['mymodule_tags_restriction'] = array(0);
  10.  
  11. // Tackle the 'view' operation.
  12. if ($op == 'view') {
  13. // Make sure the user is logged in.
  14. if (user_is_logged_in() !== FALSE) {
  15. // Extract the value of the 'field_tags' field.
  16. $current_user = user_load($account->uid);
  17. $user_tags = field_get_items('user', $current_user, 'field_tags', 'und');
  18. if ($user_tags !== FALSE) {
  19. $grants['mymodule_tags_restriction'] = array();
  20. foreach ($user_tags as $tag) {
  21. // Add the tags that the user has to the tag list.
  22. $grants['mymodule_tags_restriction'][] = $tag['tid'];
  23. }
  24. }
  25. }
  26. }
  27.  
  28. return $grants;
  29. }

Assuming that a user is given two taxonomy terms in the field_tag field we generate a key that looks like this.

realmgidview
mymodule_tags_restriction71
mymodule_tags_restriction151

One of the terms for the user matches up with the permission on the node (for the taxonomy term '7'), which therefore grants the user access to view this node. This now works as a simple taxonomy access control mechanism for the users and allows site admins to easily grant or deny access to view nodes based on taxonomy terms.

Debugging

All this is nice, but how on earth do you go about debugging this? And where do you turn when there are problems with the access that users are getting? Well it turns out that the Devel module has an understanding of this in the form of a node access block. Turning on the 'Devel node access' module will add two blocks called 'Devel Node Access' and 'Devel Node Access by User'. The 'Devel Node Access' block will show all of the access grants generated from the node itself, which essentially shows you the lock you have created. The 'Devel Node Access by User' block will show the 10 most recently active users and how their permissions relate to the current node. Devel doesn't give you any context over how the permissions are implemented with relation to hook_node_grants(), but if a user doesn't have access to the node you will see the text 'NO: no reason', and if they do you'll see 'YES: {node_access}'.

One really important thing to remember is that the access grants are only recorded when the node is saved or updated. This means that when you alter your hook_node_access_records() hook no node will understand that new structure until you save them. This can mean users being allowed access to content they wouldn't otherwise be allowed to see. Thankfully there is a function built into Drupal that will re-process all of the access hooks and essentially recreate the node_access table. The rebuild permissions admin function (located at /admin/reports/status/rebuild) will do this in a batch run, which can be a lengthy process depending on how many nodes are in the system.

I should point out that there is a hook called hook_node_access(). This allows control over content by comparing the current user and the current node. Unfortunately, this functionality is missed out of many aspects of Drupal (e.g. the main site RSS feed and the home page) so you quickly find that you have to engineer code to fill in these gaps. For example, if you were to implement a hook_node_access() call you would then need to go into all of your views and blocks and ensure that they understood the same permissions that are outlined in the implemented hook. This can quickly lead to restricted content being available without actually intending it to be. This problem is not seen with the hook_node_access_records() hook as it can be referenced by using database tagging. If the tag of 'node_access' is seen then Drupal will know that the query contains nodes that should be restricted and will force users to pass a permissions test before they can see the content. Views will use this database tag by default so you only need to worry about setting up your locks and keys and not about gaps in the presentation layer of your Drupal site.

Comments

Permalink

The best explanation of these concepts I've seen, many thanks. One small note: you may need to rebuild node access permissions (from the Status page) after implementing these hooks. I was pulling my hair wondering why it wasn't working until I did that.

Proteo (Sat, 09/22/2018 - 05:53)

Permalink

This is by far the best explanation of grant system!  Thanks a lot. I was looking for a sample use case for access control for my task n hand.

Benny (Fri, 06/21/2019 - 18:13)

Add new comment

The content of this field is kept private and will not be shown publicly.