Jan 28 2013
Jan 28

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
2
3
function mymodule_node_access($node, $op, $account) {
   return NODE_ACCESS_DENY;
}

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

LocksThe 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

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

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

Aug 20 2012
Aug 20

Managing user roles in Drupal is easy -- at least, technically easy. It's a bit trickier if you have a large user base and need to manage a steady stream of people requesting access to specific privileges, new roles, or additional responsibilities. If you're in that situation, get ready for some quality time with your email client, and set up a regular appointment with Drupal's User Management screen. Fortunately, the Apply For Role module can simplify the process.

Screenshot of Apply for Role management screen

With Apply For Role, users can visit a new tab on their user account page and request access to a new role on the site. The request is queued up for an administrator, who can review, approve, or deny requests from a central management page. Requests can also be deleted -- allowing the original user to re-submit their request later -- or denied, ensuring that they can't send in more requests for the same role.

Screenshot of Apply for Role configuration form

The module allows site builders to set up which roles can be applied for (to prevent users from getting a glimpse at roles they should never have access to), prevent or allow multiple simultaneous role requests, and so on. For site builders who want extra control, the module also provides full Views integration, as well as integration with Trigger module. You can easily build a custom administration screen to manage role applications, complete with notification emails.

There are a few noticeable gaps in Apply For Role's functionality. The application form that users fill out is spartan and lacks any explanatory text; giving administrators a way to add more help text to that page would go a long way. In addition, it would be great to customize the names of the roles that are presented to applicants. Most sites' roles are never shown to normal users, so they're often named for brevity rather than clarity. Both of those oversights can be remedied with some minor hook_form_alter() work in a custom module, but it would be great to see them integrated. Even without those wish-list items, Apply For Role is a slick solution to the problem of processing large numbers of permission-change requests. If your site's user management workflow is a good match, you should definitely check it out.

*/
Jan 18 2012
Jan 18
Lullabot logo

Lullabot has trained thousands of Drupal developers & guided the development of some of the largest Drupal websites.

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web