Figuring out User-based Node Access in Drupal (again), some notes
Simply looking at this function is one of the most useful things you can do to understand node access control in Drupal. From the 5.3 version of node.module
/**
* This function will call module invoke to get a list of grants and then
* write them to the database. It is called at node save, and should be
* called by modules whenever something other than a node_save causes
* the permissions on a node to change.
*
* This function is the only function that should write to the node_access
* table.
*
* @param $node
* The $node to acquire grants for.
*/
function node_access_acquire_grants($node) {
$grants = module_invoke_all('node_access_records', $node);
if (!$grants) {
$grants[] = array('realm' => 'all', 'gid' => 0, 'grant_view' => 1, 'grant_update' => 0, 'grant_delete' => 0);
}
else {
// retain grants by highest priority
$grant_by_priority = array();
foreach ($grants as $g) {
$grant_by_priority[intval($g['priority'])][] = $g;
}
krsort($grant_by_priority);
$grants = array_shift($grant_by_priority);
}node_access_write_grants($node, $grants);
}
What gets passed in to hook_node_grants? Let's find out.
function userreference_access_node_grants($account, $op) {
drupal_set_message('
' . print_r($account, TRUE) . '
');
drupal_set_message('op ' . $op);
return array()
}
On viewing a node, it loops three times, for ops view
, update
, delete
.
I'm not sure why delete shows up on node view, as there is no 'delete' tab. This may be a potential optimization of Drupal core.
Each time it provides a stripped down user object:
stdClass Object
(
[uid] => 0
[hostname] => 127.0.0.1
[roles] => Array
(
[1] => anonymous user
)[session] =>
)
An important thing to remember about hook_node_grants -- you don't need to return something for every realm you've defined. You only need to return something when you want to provide access (on match to the gid you're using for that realm of course).
Yeah, I had to put in a lot of debugging code in this. The old introduce new errors while trying to fix another error, so once the real error is fixed you have to go back and figure out what idiot put a 'content_' prefix in unnecessarily. OK, that took one second to spot and fix. The debugging code kept getting extended to figure out why the info was getting duplicated, and the answer is that information is stored per-field Cnaturally) and this is iterating through per content type.
Need an array_unique function or something.
function userreference_access_nids($account, $reset = FALSE, $uid = NULL) {
static $userreference_access_nids = array();
if ($reset) {
$userreference_access_nids = array();
}
if (!$account) {
if ($uid) {
unset($userreference_access_nids[$uid]);
}
return;
}
if (empty($userreference_access_nids)) {
$userreferences = variable_get('userreference_access_userreferences', array());
$userreference_data = array();
foreach($userreferences AS $data) {
$pieces = explode("+", $data);
$type = $pieces[0];
$field = $pieces[1];
$userreference_data[$type] = $field;
}
$userrefnids = array();
// can userreference fields be multiple? I'll have to look up what that would look like @TODO
drupal_set_message('' . print_r($userreference_data, TRUE) . '');
foreach($userreference_data AS $type => $field)
{
$fieldarray = content_fields($field);
$db_info = content_database_info($fieldarray);
drupal_set_message('' . print_r($db_info, TRUE) . '');
$table = $db_info['table'];
$result = db_query('SELECT nid FROM {%s} WHERE %s_uid = %d', $table, $field, $account->uid);
$object = db_fetch_object($result);
drupal_set_message('field ' . $table . ' ' . $field . ' nid' . print_r($object, TRUE) . '');
$userrefnids[] = $object->nid;
}
// assigning anything to zero is bad! It gives access to everyone.
foreach($userrefnids as $key => $value) {
if(!$value) { // $value == "" || $value == 0 || $value == NULL
unset($userrefnids[$key]);
}
}
$userreference_access_nids[$account->uid] = array_values($userrefnids);
drupal_set_message('nids: ' . print_r($userrefnids, TRUE) . '');
}
return $userreference_access_nids[$account->uid];
}
We get to decide whether the realm is even presented, so just two realms -- on for the general access and one for the per-node access -- should be all we need, with full permissions on each.
$grants = array();
$grants[] = array(
'realm' => 'userreference_access_view',
'gid' => TRUE,
'grant_view' => TRUE,
'grant_update' => FALSE,
'grant_delete' => FALSE,
'priority' => variable_get('userreference_access_priority', 0),
);
$grants[] = array(
'realm' => 'userreference_access_update',
'gid' => $node->nid, // use node id as grant id
'grant_view' => TRUE,
'grant_update' => TRUE,
'grant_delete' => FALSE,
'priority' => variable_get('userreference_access_priority', 0),
);
return $grants;
$grants[] = array(
'realm' => 'userreference_access_delete',
'gid' => $node->nid, // use node id as grant id
'grant_view' => TRUE,
'grant_update' => TRUE,
'grant_delete' => FALSE,
'priority' => variable_get('userreference_access_priority', 0),
);
Yes!
So here are both key functions:
<
pre>
function userreference_access_node_access_records($node) {
if (userreference_access_disabling()) {
return;
}
// We only care about the node if it has a userreference access field
// If not, it is treated like any other node and we ignore it.
if (in_array($node->type, variable_get('userreference_access_node_types', array()))) {
$grants = array();
$grants[] = array(
'realm' => 'userreference_access',
'gid' => TRUE,
'grant_view' => TRUE,
'grant_update' => TRUE,
'grant_delete' => TRUE,
'priority' => variable_get('userreference_access_priority', 0),
);
$grants[] = array(
'realm' => 'userreference_access_nodes',
'gid' => $node->nid, // use node id as grant id
'grant_view' => TRUE,
'grant_update' => TRUE,
'grant_delete' => TRUE,
'priority' => variable_get('userreference_access_priority', 0),
);
return $grants;
}
}
</pre>
function userreference_access_node_grants($account, $op) {
$grants = array();
switch ($op) {
case 'view':
if (variable_get('userreference_access_na_view', TRUE) || user_access('view all userreference access content')) {
$grants['userreference_access'] = array(1);
}
else {
$grants['userreference_access_nodes'] = userreference_access_nids($account, TRUE);
}
break;
case 'update': // aka edit
if (user_access('edit all userreference access content')) {
$grants['userreference_access'] = array(1);
}
else {
$grants['userreference_access_nodes'] = userreference_access_nids($account, TRUE);
}
break;
case 'delete':
if (user_access('delete all userreference access content')) {
$grants['userreference_access'] = array(1);
}
elseif (variable_get('userreference_access_na_dlet', FALSE)) {
$grants['userreference_access_nodes'] = userreference_access_nids($account);
}
break;
}
return $grants;
}
Comments
Hi, just stumbled on your
Hi, just stumbled on your page by accident.
I'm actually in the process of writing user-based field-based (and node based) userpoints access control, and going to use the user_permissions module to get true user based permissions going. It does add a lot of records for permissions in the db (each user gets his own personal role) but everything still goes through user_access() which means in theory it should work, and not have problems with views and all :)
E-mail me if you want!
Post new comment