Drupal 7 Node Access: Grants, Locks, and Keys

 

Node Access: Who wins?

While Drupal has always had a pretty robust access control mechanism, it was difficult in the past to handle multiple contributed modules who wanted to impose different types of access control. Who wins? If a node is within a private Organic Group, but is also in a public Forum, is the node private or public? In Drupal 6, multiple access control modules could conflict and had to take special care to co-exist. It was messy.

Mike Potter, Software Architect
#Drupal | Posted

Node Access: Who wins?

While Drupal has always had a pretty robust access control mechanism, it was difficult in the past to handle multiple contributed modules who wanted to impose different types of access control. Who wins? If a node is within a private Organic Group, but is also in a public Forum, is the node private or public? In Drupal 6, multiple access control modules could conflict and had to take special care to co-exist. It was messy.

In Drupal 7 the access control API was cleaned up and now it is relatively easy to handle multiple access control systems. Let's learn the best way to implement your own access control system in Drupal 7.

The Perils of hook_node_access

Drupal 7 added a cool new hook for developers: hook_node_access($node, $op, $account). On the surface, this seems like the ultimate hook to control access. You simply return NODE_ACCESS_ALLOW, NODE_ACCESS_DENY, or NODE_ACCESS_IGNORE. In reality, this hook can be very dangerous! It allows you to override the access control of any other modules on your site. For example:

  1. function mymodule_node_access($node, $op, $account) {
  2. return NODE_ACCESS_DENY;
  3. }

This would deny access to all of your content regardless of any other access control. If it returned NODE_ACCESS_ALLOW it would *allow* access to all of your content! Unless some other module returns NODE_ACCESS_DENY, in which case access would still be denied.

Even worse, your custom hook_node_access function is ignored by Views, Menus, and other content queries on the site. Even though you have denied access to all content, you'll still see all of your normal menu links, and will see your nodes listed in Views. Only when you click on a node to view it's full detail page will you then be denied. You might be violating content privacy just by showing that certain content exists!

A "Deny" based approach

Drupal is a "deny-based" access control system. In other words, if anybody denies access to a node, then the node is blocked. This is similar to having multiple locks on your door: you need to open ALL the locks to enter your door. Using hook_node_access to return NODE_ACCESS_ALLOW access violates this convention and is generally a bad idea. Instead you should design your modules to DENY access when needed, and otherwise return NODE_ACCESS_IGNORE to allow other modules to decide if access should be granted. The hook_node_access results are the "last line of defense" for denying access and don't stop Views or Menus from showing parts of the content anyway.

The correct approach is to use the Drupal "Grant" system. This API existed in previous versions, but in Drupal 7 it was cleaned up and works much better. The key hooks are hook_node_grants($account, $op) and .hook_node_access_records($node). The documentation can be hard to follow and talks about "realms" and "grant ids". Instead, let me explain this API using the concepts of Locks and Keys.

hook_node_access_records are Locks

Locks

The hook_node_access_records is called to determine if a specific node should be locked. Your module has the opportunity to create a Lock with a specific "realm" and "id". The "realm" is like the color of your lock and is typically the name of your module. This allows a single node to have multiple locks with different colors (multiple modules). To open the door, you would need keys that match each color of locks on the door.

Within a realm, you can have multiple locks with different "ids". This is like giving the colored lock a specific serial number corresponding to a key with the same color and serial number. If you have a key with the correct color and serial number, than all of the locks of that color are opened. To summarize:

  • Each lock Realm (color) must be opened to access the node
  • Only one ID (serial number) within the Realm needs to be unlocked to open that entire Realm.

These node Locks are stored in the node_access database table, which means they are cached. This table is only rebuilt when you run the Rebuild Permissions in the Status Report area of your Drupal admin. When you save a node, hook_node_access_records is called only for the node being saved to allow it's locks to be updated. If changing a node can affect the locks on other nodes, then you'll want to call node_access_acquire_grants($node) to update the locks on the related nodes.

hook_node_grants are Keys

Keys

The hook_node_grants is called to create a "key-ring" for a particular user account. This is called dynamically at each page load to determine what keys the current user has. As mentioned above, a particular node can be accessed only if the user has the appropriate keys for each Realm (color) of locks on the node. Because this key-ring is not stored or cached, it is important to make your hook_node_grants function very fast and efficient.

When implementing hook_node_grants, you are typically only concerned about the Realm implemented by your module (remember that Realm is usually your module name). You probably don't want to be messing with keys for other modules. Your hook just needs to decide if the user has any of *your* keys. Specifically, your hook needs to return a list of key IDs (lock serial numbers) within your Realm for the specified user account.

REAL Node Access!

The beauty of using the two Grant API hooks described above is that they are respected by Menus, Views, and optionally other queries within the database API. If the user does not have the proper keys to open the locks on a node, then the node will never display in any Menu or View. Unlike hook_node_access(), this properly protects the privacy of your content.

With Views, you can turn off the node access filtering in the Query Options of the Advanced section of the View. Turn on the "Disable SQL rewriting" option and now Views will return all results regardless of the keys and locks.

If you create your own database queries using the Drupal database API, you can also easily filter results based upon node access. Simply add a "tag" to the query called "node_access". For example:

  1. $query = db_select('node', 'n');
  2. ->fields('n', array('nid', 'title'))
  3. ->addTag('node_access');
  4. $result = $query->execute();

The above example would only return the nid and title of nodes the current user can access.

UPDATED: It is important to include this addTag('node_access') for ANY query that you perform that returns node results to a user. Otherwise you'll be introducing a security hole into your module. You can also use EntityFieldQuery which automatically filters results based upon node access.

An Example from Open Atrium 2

In Open Atrium 2, we implement a flexible node access system. All content is assigned to a specific "Section" within a normal Organic Group. Each Section can be locked based upon Organizations, Teams, and Users. For example, if Mike and Karen are assigned to the "Developer" Team, and the "Developer" Team is assigned to a specific Section, then only Mike or Karen can see the existence of that Section and the content within it. To accomplish this, we implement hook_node_access_records to assign locks, and hook_node_grants to assign keys.

First, let's assign the locks for content within a Section:

  1. /**
  2.  * Implements hook_node_access_records().
  3.  */
  4. function oa_node_access_records($node) {
  5. $sids = array();
  6. // handle the Section node itself
  7. if ($node->type == OA_SECTION_TYPE) {
  8. if (!oa_section_is_public($node)) {
  9. $sids[] = $node->nid;
  10. }
  11. }
  12. // Now handle pages within the Section
  13. else if (!empty($node->{OA_SECTION_FIELD})) {
  14. foreach ($node->{OA_SECTION_FIELD}[LANGUAGE_NONE] as $entity_ref) {
  15. $section = node_load($entity_ref['target_id']);
  16. if (!oa_section_is_public($section)) {
  17. $sids[] = $entity_ref['target_id'];
  18. }
  19. }
  20. }
  21. if (empty($sids)) {
  22. return array();
  23. }
  24. foreach ($sids as $sid) {
  25. $grants[] = array (
  26. 'realm' => OA_ACCESS_REALM,
  27. 'gid' => $sid,
  28. 'grant_view' => 1,
  29. 'grant_update' => 0,
  30. 'grant_delete' => 0,
  31. 'priority' => 0,
  32. );
  33. }
  34. return !empty($grants) ? $grants : array();
  35. }

For a Section node, we just grab the node ID. For pages within a section we grab the referenced section IDs. Once we have a list of section IDs, we loop through them and create a $grants Lock record giving our module name OA_ACCESS_REALM as the Realm (color), and the Section ID as the ID (serial number). This adds our colored Lock to the nodes that are protected within Sections, using the specific Section ID as the lock serial number.

Next, let's build the key-ring for the user account (*Note, this is a non-optimized version of code for instructional purposes):

  1. /**
  2.  * Implements hook_node_grants().
  3.  */
  4. function oa_node_grants($account, $op) {
  5. $sections = oa_get_sections();
  6. // returns a list of all section IDs
  7. foreach ($sections as $sid) {
  8. // determine if the user is a member of this section
  9. if (user_in_organization($sid, $account) ||
  10. user_in_team($sid, $account) ||
  11. user_in_users($sid, $account)) {
  12. $grants[OA_ACCESS_REALM][] = $sid;
  13. }
  14. }
  15. return = !empty($grants) ? $grants : array(); }

For each Section that the user is a member of, we return the Section ID for that Realm in the $grants array. If a particular node has Section locks, only users with a key to that Section will be granted access. For example, if a node has locks for $sid 1, 2, and 3, but the user only has a key for $sid 4, then access is denied. But if the user has a key for $sid 1, 2, or 3, then access is granted. You only need a single matching key within the Realm to grant access.

Conclusion

If you think about the Drupal node access system as a system of Locks and Keys, then it's pretty easy to understand. It's a very powerful system and one of the key strengths of Drupal. Try using this Grant API and only use the new hook_node_access as a last resort, especially when building other contributed modules where your hook_node_access might conflict with other modules.

Mike Potter

Mike Potter

Software Architect