Lesson #8 Class Notes wiki -- db_rewrite_sql() and the Drupal 5's node_access system

We encourage users to post events happening in the community to the community events group on https://www.drupal.org.
You are viewing a wiki page. You are welcome to join the group and then edit it. Be bold!

Drupal Dojo Lesson #8 with Andy Kirkham (ajk^)

Useful links just to get started...

Basic Node Access Control

Starting point: dojo8_1 module

We will build a module to hide nodes from anonymous users.

A clean Drupal 5.1 install, with the Devel module (to create sample
content: 5 nodes accessible to anonymous users).

We have admin user running on Firefox, with an alternative browser to
check anonymous user access as we work.

Dojo 8_1 module - Help function

We start out the dojo8_1.module file with the help function:

/**
* Implementation of hook_help().
*/
function dojo8_1_help($section) {
switch ($section) {
case 'admin/modules#description':
return t('Module 1 for Dojo lesson 8.');
}
}

Dojo 8_1 module - Install file

We create a single table with one value in it, the node id (nid):

<?php
// $Id:$
function dojo8_1_install() {
global $db_type;

switch ($db_type) {
case 'mysql':
case 'mysqli':
db_query('CREATE TABLE {dojo8_1} (nid INT(10) NOT NULL PRIMARY KEY) /*!40100 DEFAULT CHARACTER SET utf8 */');
db_query('INSERT INTO {dojo8_1} (nid) SELECT nid FROM {node}'); // Prepopulate table with current node nid values
break;
}
}

We then prepopulate this table with all the nodes in the current
system.

We now need to add to the node form a way of flagging it as a node to
be hidden from anonymous users. We will do this with some hook_form_alter() magic in the
dojo8_1.module file (all we are really doing is just adding a checkbox
to the form):

/**
* Implementation of hook_form_alter().
*/
function dojo8_1_form_alter($form_id, &$form) {

// Place a checkbox on any node-edit form.
if (isset($form['#id']) && $form['#id'] == 'node-form') {
$form['dojo8_1'] = array(
'#type' => 'checkbox',
'#title' => t('Enable anonymous user view'),
'#default_value' => $form['#node']->dojo8_1,
'#weight' => -100, // Ensure we float to teh top for visitibilty at demo time.
);
}
}

Now when we enable the module, we should see a checkbox "Enable
anonymous user view" when we edit the page.

Doing the actual work of hiding nodes from the anonymous user

To make actual use of the checkbox we use hook_nodeapi():

/**
* Implementation of hook_nodeapi()
*/
function dojo8_1_nodeapi(&$node, $op, $teaser, $page) {
global $user;

switch ($op) {
case 'load':
$r = db_query('SELECT nid FROM {dojo8_1} WHERE nid = %d', $node->nid);
return (db_num_rows($r) > 0) ? array('dojo8_1' => TRUE) : array('dojo8_1' => FALSE);
break;
case 'submit':
db_query('DELETE FROM {dojo8_1} WHERE nid = %d', $node->nid);
if (isset($node->dojo8_1) && $node->dojo8_1 == 1) {
db_query('INSERT INTO {dojo8_1} SET nid = %d', $node->nid);
}
break;
case 'view':
if ($page && _apply_access_rules() && !$node->dojo8_1) {
drupal_access_denied();
module_invoke_all('exit');
exit();
}
break;
}
}

/*
* Rule 1 used for for demo of blocking the anon user.
*/

/* helper function */
function _apply_access_rules() {
global $user;
return ($user->uid == 0) ? TRUE : FALSE; // Sample rule 1
}

Now, we prepopulated the table with all the existing nodes and on
submit, we delete it from the dojo8_1 table. If it is checked, we put it
back in again. As simple as that.

So now, if we deselect the checkbox, and the "view" op of
nodeapi is invoked by an anonymous user (through the
_apply_access_rules() helper function) attempting to view the node n
directly (i.e. http://example.com/node/n), access will be denied.

Enter db_rewrite_sql()

But what happens if there is an alternative form of viewing the node,
for example, if it is promoted to the front page, it will still appear
when invoked by list view. So for this purpose, we use the
db_rewrite_sql hook:

/**
* Implementation of hook_db_rewrite_sql()
*/
function dojo8_1_db_rewrite_sql($query, $primary_table, $primary_field, $args) {
global $user;

// returning an empty array() means "do not rewrite any SQL"

switch ($primary_field) {
case 'nid':
if (!_apply_access_rules()) return array();
return array('join' => 'INNER JOIN {dojo8_1} ON dojo8_1.nid = n.nid');
break;
}
}

Now, there has to be a matching nid in the dojo8_1 table for the node
to be seen in the list; when we uncheck a box, the anonymous user cannot
see that node promoted to the front page.

Try it! (Pastebin 11378 (dojo8_1.module), 11379 (dojo8_1.install)).
This is actually a rudimentary form of access control, using
hook_form_alter() (to add the checkbox), hook_nodeapi() (to create a
flag on the node and apply viewing access control, and
hook_db_rewrite_sql() (to implement the list view access control).

Question: When is hook_db_rewrite_sql() actually
called?

Answer: You should always wrap your database query
calls in a db_rewrite_sql() function, so that all implemented
hook_db_rewrite_sql() functions in the various modules are called. If
you don't do so, you will end up with a set of nodes that
potentially should not be accessed by the current user.

For example, consider the following (wrong) code:

$sql = "SELECT * FROM {node} LIMIT 30";
$r = db_query($sql);

The way to do it is like that:

$sql = "SELECT * FROM {node} LIMIT 30";
$r = db_query(db_rewrite_sql($sql));

if you want your module to be played ball with by other modules that
do do node access.

So just by calling db_rewrite_sql() will cause the database
abstraction layer to call all the hooks in modules which implement
hook_db_rewrite_sql().

When the list view (i.e., front page in default setting to list all
nodes promoted to front page) consults the data base to make its list,
all modules have db_rewrite_sql() hook implementations are invoked
because it wraps its calls in this function. To the amazement and
delight of even veteran drupaleros, in this lesson, we find out that you
can make custom hooks into this from any module you are writing!

Basic Taxonomy Access Control

As a slight variation we are now going to see how db_rewrite_sql()
can be used to affect the way taxonomies are displayed (a la taxonomy
access module).

We are going to do some magic on a particular user (dojo, let's
say user #2).

First, we change our access rule helper function to make it apply
only to this user:

/*
* Rule 1 used for for demo of blocking the anon user.
* Rule 2 used for the demo of rewriting user UID2's view of taxonomy selector widgets
*/

/* helper function */
function _apply_access_rules() {
global $user;
// return ($user->uid == 0) ? TRUE : FALSE; // Sample rule 1
return ($user->uid == 2) ? TRUE : FALSE; // Sample rule 2
}

So now, the access rules only apply to user 2 (not even to anonymous
users!).

We now add some code to the dojo8_1_db_rewrite_sql() function, to
affect the vid (vocabulary id):

/**
* Implementation of hook_db_rewrite_sql()
*/
function dojo8_1_db_rewrite_sql($query, $primary_table, $primary_field, $args) {
global $user;

// returning an empty array() means "do not rewrite any SQL"

switch ($primary_field) {
case 'nid':
if (!_apply_access_rules()) return array();
return array('join' => 'INNER JOIN {dojo8_1} ON dojo8_1.nid = n.nid');
break;
case 'vid':
if (!_apply_access_rules()) return array();
return array('where' => "$primary_table.$primary_field IN (1)"); // An array of hardcoded vid values.
break;

When the primary field is vid, we add a where clause to see if it is
in "1" (that is, vocabulary 1).

Now, vocabulary/1 is the only vocabulary available to user 2 (dojo).
All the other users can see the other vocabularies, but not user 2.

We can extend this idea to control which vocabulary terms are allowed
to appear:

/**
* Implementation of hook_db_rewrite_sql()
*/
function dojo8_1_db_rewrite_sql($query, $primary_table, $primary_field, $args) {
global $user;

// returning an empty array() means "do not rewrite any SQL"

switch ($primary_field) {
case 'nid':
if (!_apply_access_rules()) return array();
return array('join' => 'INNER JOIN {dojo8_1} ON dojo8_1.nid = n.nid');
break;
case 'vid':
if (!_apply_access_rules()) return array();
return array('where' => "$primary_table.$primary_field IN (1)"); // An array of hardcoded vid values.
break;
case 'tid':
if (!_apply_access_rules()) return array();
return array('where' => "$primary_table.$primary_field IN (55,56,57,58)"); // An array of hardcoded tid values.
break;
}
}

Now user dojo can only see terms 55-58.

Drupal Core's update limitation

An interesting side-effect of limiting taxonomy control.

The admin user has no limits imposed upon him as does the dojo user.
If a user who can only see a portion of the taxonomy terms updates a
page, drupal will delete all then add back the taxonomy terms... but the
limited universe of terms the limited user can see, not the full set.

The taxonomy access module gets around this:
http://cvs.drupal.org/viewcvs/drupal/contributions/modules/taxonomy_access/

Take a look at:
http://cvs.drupal.org/viewcvs/drupal/contributions/modules/taxonomy_acce...

(Log: #92355 by AjK: Node_form: preserves terms deleted by
taxonomy_node_delete() that the user shouldn't have access to
delete.)

Two functions were added:

  1. function taxonomy_access_preserve_terms()
  2. function taxonomy_access_restore_terms() 

What the first function does is to intersect the full set of terms
available on the node (by bypassing db_rewrite_sql() with the set of
terms the user can see taking into account her access control. This gets
inserted into a variable available across the session, so that when
update is invoked, a restore is carried out. In order for this to work,
the module's weight is adjusted to put it at the bottom of the list
so drupal core doesn't delete all the nodes.

Drupal uses the module weights to order the modules, default is zero.

So if you ever run into this problem, taxonomy_access is the place to
come and look to see a solution.

Drupal 5's node_access system

Introducing Drupal's node_access table

We switch off the drupal8_1 module now.

We are going to examine the node module and see what it does with
node access and the node_access table.

describe node_access;

Field Type Null Key Default Extra
nid int(10) unsigned NO PRI 0
gid int(10) unsigned NO PRI 0
realm varchar(255) NO PRI
grant_view tinyint(3) unsigned NO 0
grant_update tinyint(3) unsigned NO 0
grant_delete tinyint(3) unsigned NO 0

gid: grant id.

grant_* are the flags.

By default, there is one record in the table: without it, you
wouldn't see any nodes at all.

See http://api.drupal.org/api/5/function/node_access (modules/node/node.module, line 2622).

node_access() code review

Two simple rules are applied straight out:

  1. If you have "administer nodes" access you are granted
    access directly.
  2. If you have not got "access content" you are denied
    access directly.

node_access determines these privileges via the user_access function.
What is user_access()? See http://api.drupal.org/api/5/function/user_access
.

It is the function to determine whether the user has a given
privilege. It checks the permissions available to that user.

After applying the two simple rules above, node_access then asks all
node modules (i.e., modules that implement a node) that implement
hook_access for a true/false/null on access. (Reference: see http://api.drupal.org/api/5/function/hook_access
). For a module implementing a widget node, we would be looking for a
widget_access() function. If true or false is returned, that condition
is returned outright. If null is returned, we use Drupal's default
settings.

The node_access table is now examined for all but the create op (that
access should be determined by the node module, as per last paragraph).
We are looking at view/update/delete grants. (See table description
above). The single default record in the node_access table determines
that by default, grant_view is on, while grant_delete and grant_update
is on.

The node value in this record is set to 0, covering all nodes not
covered by any access control (view yes, update/delete no).

That is what is returned in that case.

forum_access module

We will now enable the forum_access module and see what happens to
the node_access table.

(to install, you must also download the required acl module):

We enable the required ACL module first, then Forum Access (which
will automatically invoke the enabling of the required core forum
dependency).

The node_access table now looks very different:

nid gid realm grant_view grant_update grant_delete
2 0 all 1 0 0
3 0 all 1 0 0
4 0 all 1 0 0
5 0 all 1 0 0
6 0 all 1 0 0
7 0 all 1 0 0
8 0 all 1 0 0
9 0 all 1 0 0
10 0 all 1 0 0

The nid 0 has been replaced by a number of additional records. Just
as we pre-populated our custom dojo8_1 module table with all nodes, to
establish node by node access control. Here Drupal does the same.

The function actually responsible for doing so is node_access_rebuild
http://api.drupal.org/api/5/function/node_access_rebuild

To see how we got here, let's have a quick look at the
forum_access module code, specifically the forum_access_enable()
function:

/**
* Implementation of hook_enable
*/
function forum_access_enable() {
node_access_rebuild();
}

So this function is called when the module is switched on for the
first time.

node_access_reubuild (see code in API reference just above) first
deletes everything from the node_access table (the default rule), then
all node_grant module functions are invoked. This can be quite an
expensive operation, since we have to loop through all the nodes and
process the list of grants obtained from every node (invoking all the
pertinent module hook_access_node_grants functions.

What this module is doing is associating user roles with grants.

One very important point is that the node_access table should be
written to only by the node module, other modules should not write to it
directly. It is written to by the node_access_acquire_grants function: http://api.drupal.org/api/5/function/node_access_acquire_grants

"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."

This has effectively ended the free-for-all on the node_access table.
Drupal 5 introduced this API.

So in the forum access module, the following:

function forum_access_node_grants($user, $op) {
$grants['forum_access'] = array_keys($user->roles);
return $grants;
}

returns the role ids as the grant ids for the 'forum_access'
realm.

So the table has been populated via the hook_access_enable
implementation. The invoked node_access_rebuild() function will reinsert
the default record if no module offers node access rules.

So if we disable forum_access, and rebuild the node_access table by
means of a visit to Administer >> Content Management >> Post
settings and hitting the "Rebuild permissions" button, we will
see only the default record in node_access table (otherwise no-one will
be able to access any content, as explained above in the review of the
node_access function).

Go ahead and try it (not at home by yourself, and certainly not on a
production site!): delete all records, and you get the "Welcome to
your new Drupal website!" page.

During the class, an attempt was made to then re-insert the default
row by rebuilding permissions through the administration interface.
Unexpectedly, this didn't work. :)

So we copy the $sql statement from node_access_rebuild (see reference
above):

 db_query("INSERT INTO {node_access} VALUES (0, 0, 'all', 1, 0, 0)"); 

and stick that into the database and all should be well again. We are
back in business again.

Before re-enabling the forum_access module, we review the code.

We have already seen that forum_access_enable() will be called when
the module is first enabled, and that rebuilds the node_access table by
populating it with default grants for all existing nodes, by invoking node_access_rebuild, which deletes the default
record and invokes the node module's node_access_acquire_grants
function for every node on the system (as mentioned above, a potentially
very time consuming process).

We re-enable the forum_access module, and now in the node_access
table the default record is gone, and there is a row for every node,
instead of just a single row for all nodes (node 0). So now the
node_access module will match individual nodes to check on access
grants.

Now, node_access_acquire_grants actually invokes hook_node_access_records(). The forum_access module
has its own table (forum_access, which mirrors node_access, see
forum_access.install) in its implementation of this hook.

The tid column (forum, based on taxonomy term), the rid (role ids),
and the four grant_* flags.

Now, by default, no actual grants are enabled, so that node_access_acquire_grants will actually return the
default:

  $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);
}

Now, heading over to an existing forum, when we edit it we see there
is a new section, "Access control", which allows you to grant
access to the various roles for viewing, posting, editing and deleting
posts to the forum.

So we remove anonymous user grants for all ops. If we look at the
forum_access table we can see there are changes reflecting what we have
done. Now there is an entry for each role, with the appropriate grants
(view, update, delete, create) for each.

Now, the forum_access module just happens to use the role id for the
grant id, which is quite common in this kind of module. But you could
create your own groupings. Since roles already exist and are part of the
system, it makes sense to use them.

The node_access table also has some additions, specifically in
relation to the forum_access realm, for which grants have been added in
for the various roles as specified in the forum administration settings.

So now, node_access_acquire_grants will be returned grants
when it invokes form_access' implementation of
hook_node_access_records:

function forum_access_node_access_records($node) {
if (!forum_access_enabled()) {
return;
}

if ($node->type == 'forum') {
$result = db_query('SELECT * FROM {forum_access} WHERE tid = %d', $node->tid);
while ($grant = db_fetch_object($result)) {
$grants[] = array('realm' => 'forum_access', 'gid' => $grant->rid, 'grant_view' => $grant->grant_view, 'grant_update' => $grant->grant_update, 'grant_delete' => $grant->grant_delete);
}
return $grants;
}
}

Here, our own personal grant table (forum_access) is consulted and
those results are returned.

There do exist priorities among grants, in order to prevent clashes
between different modules using node_access (again, see node_access_acquire_grants).

The key to all of this is the following line of code in the node
module node_access function:

$sql = "SELECT COUNT(*) FROM {node_access} WHERE (nid = 0 OR nid = %d) $grants_sql AND grant_$op >= 1";

This is what links the node_access table to the nodes.

So, moving right along to the third forum_access module function,
forum_access_form_alter, we see it is responsible for the checkbox
interface with which forum access is specified in the editing of each
forum (see line 168).

There is a second section, powered by ACL (access control lists
module), the moderator section. This works by ACL allowing you to
specify access lists, which are essentially groups of users that can do
certain things. For our forum moderator, we see the code at line 191
onwards. We can see a little further down that acl_create_new_acl is
invoked, which creates the access control list based on the term id
(tid) if none exists. So when the forum administrator adds an additional
user as forum moderator in the moderator section, that user gets added
to the ACL.

Another item of interest is the hook_nodeapi implementation, which
makes sure in the case of inserts that ACL data is added to fresh posts.

That pretty well covers the access aspect of the forum_access module.

node_access_example module

Available on cvs: http://cvs.drupal.org/viewcvs/drupal/contributions/docs/developer/examples/
Check out the three files: node_access_example.module, .info, .install

Module code can be read here: http://cvs.drupal.org/viewcvs/*checkout*/drupal/contributions/docs/developer/examples/node_access_example.module?rev=1.6

Working your way through the code in this module will really show you
the ins and outs of how data flows through the node_access table.

The module itself is less than 200 lines long, and it has everything
you need to know about how to get things in and out of that node_access
table.

The key is the magic line of code in the node module's
node_access function:

$sql = "SELECT COUNT(*) FROM {node_access} WHERE (nid = 0 OR nid = %d) $grants_sql AND grant_$op >= 1";

The key to your own module's success in using it can be found in
this example module and the grants it creates:

function node_access_example_node_grants($account, $op) {
if ($op == 'view' && user_access('access private content', $account)) {
$grants['example'] = array(1);
}

if (($op == 'update' || $op == 'delete') && user_access('edit private content', $account)) {
$grants['example'] = array(1);
}

$grants['example_author'] = array($account->uid);
return $grants;
}

ACL module

The access control lists module is basically a helper module, which
is all about what user has access to what nodes.

Question: Does ACL have the group concept?

Answer: ACL does not have the group concept. You
could use the acl to form groups, linking it to the buddylist module.

A handy example here (from our very own team member Agustin
Kanashiro! --vk): http://groups.drupal.org/node/1688#comment-8582
(see following discussion).

This is the forum_access module reworked around the buddylist module.

Question: Is it possible to show posts only to
buddies in Drupal 4.7?

Answer: I don't see why not... The problem you
are going to have is that if you are using node_access then you
won't be able to use Taxonomy Access or OG access. If you wrote your
own module with its own version of the node_access table, then it would
just be a matter of killing node_load and nodeapi.

Taxonomy doesn't have its own access system, so the only way to
provide access control for them is to rewrite the SQL using
db_rewrite_sql(). See forum_access.module:

/**
* Because in order to restrict the visible forums, we have to rewrite
* the sql. This is because there isn't a node_access equivalent for
* taxonomy. There should be.
*/
function forum_access_db_rewrite_sql($query, $primary_table, $primary_field, $args) {
if ($primary_field == 'tid' && !user_access('administer forums')) {
global $user;
$roles = _forum_access_get_roles($user);
$sql['join'] = "LEFT JOIN {forum_access} fa ON $primary_table.tid = fa.tid LEFT JOIN {acl} acl ON acl.name = $primary_table.tid AND acl.module = 'forum_access' LEFT JOIN {acl_user} aclu ON aclu.acl_id = acl.acl_id AND aclu.uid = $user->uid";
$sql['where'] = "(fa.grant_view >= 1 AND fa.rid IN ($roles)) OR fa.tid IS NULL OR aclu.uid = $user->uid";
$sql['distinct'] = 1;
return $sql;
}
}

Question: Would one be better off moving to Drupal 5
for customized handling of node access?

Answer: Yes.

"Before Drupal 5, writing node access code was tricky... well,
you were just writing to the node_access table directly... which meant
you might not play nice with other modules that were writing to it at
the same time... that's where the priorities come in, where values
are merged together..." (ajk^)

To go on with ACL...

acl_create_new_acl() creates a new list. In the tables themselves:

SELECT * FROM acl
acl_id module name
4 forum_access 60

The name is actually the tid of the forum you are controlling.

There follow in the acl.module code functions to delete a list and to
add users to the list.

There is a node in the forum (a post) with its access controlled by
this access control list (4):

SELECT * FROM acl_node
acl_id nid grant_view grant_update grant_delete
4 58 1 1 1

We now edit the forum and add a user dojo as moderator.

TODO: see how this affects acl_user table.

TODO: show how the form_access interacts with the acl module
(function calls, etc).

TODO: Show the the form_access module bolts in its form with a single
call to the acl_edit_form() function. Show how the list of users are
serialized and unserialized with acl_edit_form() and acl_save_form().

TODO: Review remaining ACL module functions and explain their use.

 

AttachmentSize
drupal_pastebin_11378.php_.txt1.86 KB
drupal_pastebin_11379.php_.txt2.25 KB