I've read that improved node access is on the wishlist for Drupal 7. After seeing another stab at this request here, I spent a few hours thinking. I don't think a logic patch is the right approach: it is difficult to understand even for admins, let alone users, it is difficult to make work right, and worst of all, it solves the problem by damaging current functionality.

Right now we have the node_access table, which is the source of the problem. We have two values we can write in it: 1+ for allow and 0 for deny. The problem is that deny is weak: if any other module allows access, the deny is overruled. Actually, deny should be read as "undecided, defaults to deny". Obviously, there are use cases where a strong deny is needed, where deny should overrule allow.

My first thought was to alter the node_access table to accept negative values and use those for deny. I'm certain it would work beautifully, and node access modules would be able to really say what they mean when they say no. The problem is that this would make the already inefficient node_access table even larger: currently 0 values don't have to be written, but -1 values would have to be. Therefore I considered an option which I think is even better: getting rid of the node_access table completely.

The following code is my alteration to node_access():

  // If the module did not override the access rights, call implementations of                                                                                                                    
  // hook_node_access() on all modules.                                                                                                                                                           
  if ($op != 'create' && $node->nid) {                                                                                                                                                            
    $grants = module_invoke_all('node_access', $op, $node, $node->status, $account);                                                                                                              
    $deny_count = 0;                                                                                                                                                                              
    $allow_count = 0;                                                                                                                                                                             
    foreach ($grants as $grant) {                                                                                                                                                                 
      if ($grant === FALSE) {                                                                                                                                                                     
        // Deny request.                                                                                                                                                                          
        $deny_count += 1;                                                                                                                                                                         
      } elseif ($grant === TRUE) {                                                                                                                                                                
        // Allow request, unless denied by another module.                                                                                                                                        
        $allow_count += 1;                                                                                                                                                                        
      } // Undecided, deny by default unless allowed by another module.                                                                                                                           
    }                                                                                                                                                                                             
    if ($deny_count) {                                                                                                                                                                            
      return FALSE;                                                                                                                                                                               
    } elseif ($allow_count) {                                                                                                                                                                     
      return TRUE;                                                                                                                                                                                
    } elseif (count($grants)) {                                                                                                                                                                   
      return FALSE;                                                                                                                                                                               
    } else {                                                                                                                                                                                      
      if ($op == 'view') {                                                                                                                                                                        
        return TRUE;                                                                                                                                                                              
      } else {                                                                                                                                                                                    
        return FALSE;                                                                                                                                                                             
      }                                                                                                                                                                                           
    }                                                                                                                                                                                             
  }                                                                                                                                                                                               

And here is the code for the new hook, hook_node_access():

?php
/**
 * Allow or deny a given operation on a node.
 *
 * When a node is accessed, a module implementing node access will be asked
 * for its opinion whether the operation should be allowed on the node.
 * The module must respond with either TRUE to allow the operation, FALSE
 * to deny it, or NULL if it is undecided.
 *
 * @param $op
 *   The node operation to be performed, such as "view", "update" or "delete".
 * @param $node
 *   The node object on which the operation is to be performed.
 * @param $published
 *   Indicates whether the node is published. Operations on unpublished nodes
 *   should generally not be allowed.
 * @param $account
 *   The user object performing the operation.
 *
 * @ingroup node_access
 */
function hook_node_access($op, $node, $published, $account) {
  // Allow viewing of all pages and deny deletion of pages.
  if ($node->type == 'page' && $published) {
    if ($op == 'view') {
      return TRUE;
    } elseif ($op == 'edit') {
      return NULL;
    } elseif ($op == 'delete') {
      return FALSE;
    }
  }
  return NULL;
}
?>

The way it would work is that each node access module must implement the new node_access hook. This hook implementation receives as arguments the operation, node, published status and user performing the operation. The published status is of course part of the node object, but I made it an argument to highlight its importance. Depending on this information - and its own settings - the node access module could return a clear decision: allow, deny or undecided, defaults to deny. If any module returns deny, it overrules any allows. Otherwise, allow overrules undecided. If all modules are undecided, access is denied. Only if no values are returned, meaning there are no node access modules active, do we use default permissions: allow view and deny everything else.

To me, this solution looks simple and potentially very efficient. Currently we have the bloated node_access table from which we have to search records - here we ask the node access modules directly what access permissions they have for the node. Of course, if the module requires complex and expensive database queries to determine its return value the efficience advantage is lost, or at least diminished. But, in other cases you wouldn't need large queries at all: you could make a node access module which always returns NULL for operations other than 'view' - to be used only for determining view access. Some modules could be made to manage access of a certain type of content only, and they could return NULL immediately without a single database operation.

The one undisputable advantage of this approach is obvious: no more need for expensive node access rebuilds. Also, the confusing system of priorities - which few modules ever used to their full potential - would become unnecessary.

Of course I am aware that a change like this would have far-reaching effects. I haven't even attempted to touch everything that would be affected by this - in this opening post, I simply wanted to explain my approach. The node_access table has another use for the db_rewrite_sql() function, so that would need to be patched also. Other functions would become deprecated, and would need to be removed.

So, let's hear some critique. I'm sure there is some due.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

mantyla’s picture

Darn, misformatted the first piece of code. Please check the node_access.patch for an ungarbled version.

Dave Cohen’s picture

Your code centers around node_access calls, when you have a node loaded and want to know whether the user can view it. The node_access table addresses a different but related problem. That problem is displaying a long list of node titles.

Say you build a Views table with node titles. To build the page Drupal generates a query which joins the node_access table. That way, the database returns the nodes the user can see. Otherwise you'd have to load all those nodes to determine which are visible. While the node_access table joins have some performance problems, they are better than that alternative.

I think it will remain difficult to build a module which helps coordinate the efforts of multiple node_access modules. But it's not so hard to build your own node_access module with the exact custom logic you need. All these modules need to keep in mind "only grant permission if your positive the user has that permission".

For Drupal 7, I'd like to see the node_access table get rid of the 'view', 'update' and 'delete' grant columns and replace them with a more flexible 'op' column which would have values like 'view', 'view unpublished', 'view revisions', 'update', 'delete', etc... I'd also like to see a hook_node_access_records_alter(). This would allow modules to adjust the values returned by other modules before the node_access table is written.

mantyla’s picture

I'm aware of the problem with node lists. I did mention db_rewrite_sql() needing a rewrite also, and to be honest, even as I posted this topic I was uncertain if it could be done without node_access.

However, I also think that we need a strong deny. I agree that it's not so hard to build a node access module with the exact logic you want - I've done this working on the Nodeaccess module myself. The problem is knowing what other modules think. Taxonomy Access is a prime example: it wants to use a strong deny, but it is unable to do so. The only way to remedy this is to customize modules, which is a daunting task even for experienced developers. We shouldn't be forced to re-invent the wheel, when we already have two perfectly good wheels and it is the axis that has the problem.

But, recognizing the problem with my own approach, I also know a way to make it better: as you said, the reason the node_access table is really needed is to determine view access when using large lists. So why should we use it to store other ops at all? We could use node_access - maybe renamed to node_view_access or something - for view access for node lists only. Without the edit and delete columns it'd be more lightweight, and the advantages of my approach would still be there with other operations.

Your suggestion of custom operations would be realized this way as well: since the operation is forwarded to the hook_node_access implementation, there is no limit to the ops these implementations could recognize. Rather than hardcode the extra ops by giving them their own columns, I think this approach would ensure the maximum amount of freedom.

I'll need to think about this some more and get back to it.

mantyla’s picture

FileSize
3.96 KB

Ok, time to make this more simple. I re-evaluated my original idea and implemented that with surprising ease. I wanted to add a way for node access modules to have a strong deny, and since node lists require a simple way of figuring out node access, there is no way around needing the node_access table.

My solution was to extend that table to allow negative values. A negative value equals a deny, only it takes precedence over allow, and unlike a zero value, it needs to be written. I'm uncertain if this will have a major impact on the size of node_access, but I imagine most modules will continue to use zero values which don't need to be written, and use negative values only when absolutely necessary.

The logic is that deny takes precedence over allow, and allow takes precedence over undecided (zero) value, which defaults to deny.

This required adding another query into node_access() to check for negative values. It also required adding a check into _node_access_where_sql(), so roughly the patch made node access determination twice as expensive as before - feel free to criticize me for that, but I can't think of a way to help it and keep things working. There was also a minor change into node_db_rewrite_sql(), since it needed to pass an argument it didn't do before.

I also reversed the order of author view access checking and other access checks. The idea of it was to prevent the possibility that an author could always see nodes that were unpublished, but nodes that were published could be hidden from the author.

I gave some consideration to the fact that core doesn't need db_rewrite_sql() for any other operations than view, so technically you could use a node_view_access table for determining view access only and hook function calls for other operations. But I'm not sure if any module depends on db_rewrite_sql() working with other operations, so to be safe, I had to leave node_access as it was, save adding negative values.

I didn't include a schema patch or an update function, both of which are still necessary. This is enough for now.

Dave Cohen’s picture

Status: Needs review » Needs work

A few things, just my opinions...

So the API as it is now permits "strong allows". You prefer it permits "strong denys". In doing so, you take "strong allows" away. I don't see why either is better than the other, but I see a big problem with switching the way it already works.

I'm not sure if any module depends on db_rewrite_sql() working with other operations

They do. For example tac_lite uses it to hide taxonomy terms.
If you wanted, you could write a third-party module that introduces a node_deny table and use db_rewrite_sql() to hide nodes listed there. That's one way to introduce "strong deny" without breaking sites that already work.

What did you think of my hook_node_access_records_alter() idea. Is there anything you need to do that could not be accomplished with that?

This sort of logic should not be baked into core:

+  // Let authors view their own nodes.
+  if ($op == 'view' && $account->uid == $node->uid && $account->uid != 0) {
+    return TRUE;
+  }
the node_access table is really needed is to determine view access when using large lists. So why should we use it to store other ops at all?

1) I want to list nodes an administrator can edit.
2) I want to list unpublished nodes the administrator is allowed to delete.
3) I want to list nodes with revisions under moderation.
and so on. There are things like this not easily done today and changes to node access should make those sorts of things possible.

moshe weitzman’s picture

Status: Needs work » Needs review

there are use cases for explicit deny and for explicit approve (our current situation). it would be nice to support both in a clean way.

mantyla’s picture

It is not that I would want to take strong allows away. But, I know there are use cases where a strong deny is needed instead, so I wanted to give node access modules the option of using either. With my patch we'd have both a strong and a weak deny, and allow would be between them.

They do. For example tac_lite uses it to hide taxonomy terms.

I had a look at db_rewrite_sql() and I can't see any place where it would take the operation into account. node_db_rewrite_sql() only checks view access for lists, so currently we have no core-built method of checking edit or delete access for large lists.

My change only applies to uses of db_rewrite_sql() where the $primary_field is nid, since node_db_rewrite_sql() only responds to that. In the tac_lite example the $primary_field is tid, so it would not be affected. Since they are both implementations of hook_db_rewrite_sql(), it remains of course possible, even easy, to write an implementation that checks edit or delete access for nodes, and with my changes, _node_access_where_sql() could still be used as a building block for making such an implementation.

I admit that just because I can't imagine a use case where someone would need a list of editable or deletable nodes without having an administrative permission doesn't mean someone else couldn't. You've got me convinced that edit and delete need to stay in the node_access table.

If you wanted, you could write a third-party module that introduces a node_deny table and use db_rewrite_sql() to hide nodes listed there.

True. But it would be for lists only. I would want this to work with the node_access() function as well, and no hook function lets me do that.

As for hook_node_access_records_alter(), I'm not sure, but I fear it would be messy. Writing such a function would require detailed information about how other node access modules work, which is of course available, but still takes work. Also, exactly what would it be used for? We already have priority, which is sadly underused - nearly all node access modules I've seen use a hardcoded value of 0 for priority. Using priority you can achieve a strong deny, or something similar, and if such a simple approach has not been adopted, why would a more complex one be?

Speaking of priority: what should be done about that? Should it be kept at all? If we have the chance of strong denies its reason for existence is severely diminished, and it has always been logically flawed. Suppose you wanted a module to use a strong deny to prevent anyone from deleting a certain type of content, but potentially allow editing of it. This can't be done with priority, since if you use a high priority to prevent anyone else from granting delete, you also prevent anyone from granting edit since they are on the same row. I say we either get rid of priority completely or split the node_access table into operation-specific tables.

Oh, and it's not that I would want to use a strong deny for anything. I'm perfectly happy using only weak denies with the Nodeaccess module, which I maintain. But other modules may want to, and I know Taxonomy Access Control does.

This sort of logic should not be baked into core:

I'm inclined to agree. But, it already is a part of core, I simply moved it to a different location for sake of consistency. I don't think it would make sense that an author can see unpublished content, but there's a possibility he doesn't see it any more after it's published.

Anonymous’s picture

Status: Needs review » Needs work

The last submitted patch failed testing.

mdupont’s picture

Isn't it kind of fixed already in D7 since any module can now implement hook_node_access() to use it on any node (in D6 it was limited to content types defined by the module)?

If one wants to implement custom logic one can do so there, using NODE_ACCESS_DENY to make a strong deny, and hook_query_node_access_alter() to filter node lists.

mdupont’s picture

Title: Node access by multiple modules » Improve Node Access system
Version: 7.x-dev » 8.x-dev

Feature requests go against 8.x-dev. Node Access system is way better in D7 and D8 than in D6 but I guess node_access table could use some further improvements.

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.8.x-dev

Drupal 8.6.x will not receive any further development aside from security fixes. Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.2.x-dev

Drupal 8 is end-of-life as of November 17, 2021. There will not be further changes made to Drupal 8. Bugfixes are now made to the 9.3.x and higher branches only. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.15 was released on June 1st, 2022 and is the final full bugfix release for the Drupal 9.3.x series. Drupal 9.3.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.4.x-dev branch from now on, and new development or disruptive changes should be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.9 was released on December 7, 2022 and is the final full bugfix release for the Drupal 9.4.x series. Drupal 9.4.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.5.x-dev branch from now on, and new development or disruptive changes should be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

smustgrave’s picture

Status: Needs work » Postponed (maintainer needs more info)

What, if anything, needs to be updated for D10? If no longer relevant please close out. If relevant update issue summary.

Thanks!

Version: 9.5.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.