Dispelling Node Access Fears

Venturing into the node access realm of hooks and functionality for some reason always felt complex, so I have avoided it complete thus far, relying on various node or taxonomy access based modules (http://drupal.org/taxonomy/term/74), to handle any access issues I needed. This usually resulted in systems that were more complex or more burdensome to maintain than desired. Well, I was wrong, the node access system isn't complex, and it is powerfully efficient and optimized.

Recently, I needed to implement a system to allow editing access to a node (an organization node) by multiple users, but we wanted the original node author to dole out this access instead of sending requests to the administrators. But beyond that we needed them to be able to edit all the 'product' nodes that referenced the 'organization' node.

A couple modules were close to what I needed (http://drupal.org/project/nodeaccess_autoreference, http://drupal.org/project/nodeaccess_userreference, http://drupal.org/project/nodeaccess_nodereference) but they were a bit more featured loaded than I wanted, and I used this as an opportunity to learn about the node access hooks (check out the node access example for a general example.).

To tackle my issue, I started with a user reference CCK item on the organization node, so the node author can assign other members to the node, and also added a node reference item to the product nodes to reference an organization node.

Now that the links were in place, I had to implement the node access permissions. Even time a node is checked for 'edit' permissions the node grants hook is run, so my module needed to implement this hook.

function example_node_grants($account, $op) {
   //Keyed array with realm as key
   $grants['referenced'] = array($account->uid);
   return $grants;
}

From the API guide, I just needed to create an array with the key as the realm of access permissions (should not conflict with any other node access module you have, if any). I called the realm 'referenced' since I was using node and user references, and since it was user based, I just use the user ID as the item to look for.

In simple terms, this tells the node access system to look for an entry that links the user ID, with the requested node ID in the 'referenced' realm of entries.

But the check is useless unless we have already added grants to the system, so the node access record hook is needed.

Checking the API guide, I just needed to pass an array of arrays with the grants that needed to be saved.

function example_node_access_records($node) {
   if ($node->type == 'org') {
   //For each user referenced in the CCK item, add a grant entry.
   foreach($node->field_members as $u) {
     if (!empty($u['uid'])) {
       $r_user = user_load($u['uid']);
       $grants[] = array(
         'realm' => 'referenced',
         'gid' => $u['uid'],
         'grant_view' => TRUE,
         'grant_update' => TRUE,
         'grant_delete' => user_access('administer org', $r_user) ? 1 : 0,
         );
       }
     }
   }
   elseif ($node->type = 'product') {
     //For each 'org' node referenced, check for all user references to add grants.
     foreach($node->field_org_ref as $o) {
       if (!empty($o['nid'])) {
         $o_node = node_load($o['nid']);
       //For each user referenced in the CCK item, add a grant entry.
       foreach($o_node->field_members as $u) {
       if (!empty($u['uid'])) {
         $r_user = user_load($u['uid']);
         $grants[] = array(
         'realm' => 'referenced',
         'gid' => $u['uid'],
         'grant_view' => TRUE,
         'grant_update' => TRUE,
         'grant_delete' => user_access('administer org', $r_user) ? 1 : 0,
         );
         }
       }
       }
     }
   }

   return $grants;
}

When a node is saved, all the grants for that node are re-calculated. But I had an issue where I needed to also resave all the referenced 'product' nodes with new permissions each time the 'org' node was updated. So I needed to also make a nodeapi hook entry to save new grants for any referenced 'product' nodes.

function example_nodeapi(&$node, $op) {
if ($op == 'update' or $op == 'insert') {
//Update node access table with grants of the author and members of an org
if ($node->type == 'org') {
//Add grants for all products that reference this node.

$qry = db_query("SELECT DISTINCT(nid) FROM content_type_product WHERE field_org_ref_nid = %d",$node->nid);
while ($result = db_fetch_object($qry)) {
$grants = array();
foreach($node->field_members as $u) {
if (!empty($u['uid'])) {
$r_user = user_load($u['uid']);
$grants[] = array(
'realm' => 'referenced',
'gid' => $u['uid'],
'grant_view' => TRUE,
'grant_update' => TRUE,
'grant_delete' => user_access('administer org', $r_user) ? 1 : 0,
);
}
}
node_access_write_grants($result, $grants, 'referenced');
}
}
}
}

The node_access_write_grants function takes the same array entries like the node access records hook. It isn't needed to be called oin the node access records hook because the grants are written later through the hook loop, but in the nodeapi() implementation, we have to explicitly call the write grants function to make sure they are saved.

To round out the permissions carefully, I turned on the CCK permissions modules and protected both the user reference and node reference fields so that members with this new delegated power can't add or remove other people from the editing powers. Only the node author can do that.

Now that I have done it once, I won't be nearly as fearful regarding the node access system, and I have found how optimized this system is. Great job Drupal core developers.

It is true that you need to

It is true that you need to know more PHP than a designer ever wants to know, but if you get a good theme it is well and good.
Regards,
cctv calculator