May 21 2015
ChX
May 21

Drupal 7

In Drupal 7, a hook_node_access implementation could return NODE_ACCESS_IGNORE, NODE_ACCESS_ALLOW and NODE_ACCESS_DENY. If any of them returned NODE_ACCESS_DENY then access was denied. If neither did but one returned NODE_ACCESS_ALLOW then access was allowed. If neither of these values were returned by any implementation then the decision was made based on other rules but at the end of the day some code needed to grant access explicitly or access was denied. Other entities didn’t have access control.

Also, blocks had some sort of access control in a very strange way: hook_block_list_alter is used -- even by core -- to remove the non-visible blocks.

Drupal 8

Drupal 8 brings a unified entity API -- even blocks become entities. It also uses many of the same objects and concepts for routing access. Instead of constants, we now use objects implementing the AccessResultInterface. You can get an instance by calling the rather self descriptive AccessResult::allowed(), AccessResult::forbidden(), AccessResult::neutral() methods. If you are handed an AccessResultInterface object you can figure out which one it is by calling the isAllowed, isForbidden, isNeutral methods on it. Only one of them can return TRUE. Access results can be cached and so have relevant cache contexts and tags -- this is why we bother with Neutral. This caching metadata is properly merged when doing various operations on access results.

These objects are returned by hook_entity_access implementations for entity access check and by the check method of services tagged with access_check used for routing access

Let’s first consider a node access case. The entity access checker fires hook_entity_access and hook_ENTITY_TYPE_access (in this case hook_node_access) and tries to put together the results in a sane way. If we do not want to change the behavior from D7, then any D7-deny (now called Forbidden) should result in a Forbidden end result. If there are no Forbiddens then any Allowed should result in a final Allowed return value. Finally, if there were neither Forbiddens nor Allows then only Neutral were present (if anything at all) so return a Neutral. This is called the orIf operation, if you have two result objects then run $result1->orIf($result2) to get the end result. The other doesn’t matter, $result2->orIf($result1) is the same.

Let’s take a look at the AccessResult::allowedIfHasPermission method. What results do we want? Obviously, if the permission is present we want an Allowed to be returned. But if the permission is not there? It can not result in Forbidden because then any hook_node_access simply returning the result of this method would immediately deny access to the node. It really can only return Neutral: the lack of a permission means we this method can’t form an opinion about access. (Using this method is strongly preferred to if ($user->hasPermission()) { return AccessResult::allowed();} because it adds the right caching metadata).

Let’s say we want to determine whether a field is editable! This requires the entity to be editable and the field to be editable. For the sake of simplicity, let’s presume both are controlled by a permission. So let’s say the user has “edit entity” permission but doesn’t have the “edit field” permission. So according to the previous paragraph AccessResult::allowedIfHasPermission($account, ‘edit entity’) is Allowed while AccessResult::allowedIfHasPermission($account, ‘edit field) is Neutral. We can not return Allowed in this case! So we can’t use orIf -- we need another operation: this is called andIf. Much like orIf we want any Forbidden input to result in a Forbidden output and again the same as orIf we want two Allowed to result in Allowed. The only difference is in the case detailed above: when one is Allowed, the other Neutral, here orIf results in Allowed but andIf results in Neutral.

If you want to use your knowledge and familiarity with AND/OR then consider first the iron rule of “any Forbidden input results in Forbidden” and only if that rule didn’t apply then consider Allowed as TRUE and Neutral as FALSE and apply the normal AND/OR to these values. This logic is called Kleene's weak three-valued logic where the “third value” is Forbidden. Most misunderstanding and confusion results from trying to treat Forbidden as FALSE instead of being the contagious “third value” it is. The name Neutral might make you think “oh, three values are not a problem, I will just erase any N I see and the resulting two values can be evaluated like normal AND/OR” this is absolutely incorrect! In fact, if you have two variables and both are Allowed / Forbidden then the results for $x->orIf($y) will be the exact same as $x->andIf($y)! The outcome will only differ if either $x or $y is Neutral (and the other is Allowed).

Routing vs Entity Access

We have clarified the necessity for two operators and we have two subsystems, each using one of them: routing uses andIf, entity uses orIf. The difference is subtle -- as we have seen the only difference is the end of result of two access checks where one is Neutral, the other is Allowed. This becomes a Neutral result in routing and an Allowed result in entity access.

Making a Decision

All this three value logic is nice but at the end of the day, this is all internal. The user wants to see a page, can we show it to them or do we show a 403 page? Can we show this entity? The answer cannot be “I do not know”, it must be yes or no. So the system looks at the result and says yes if it is explicitly allowed, otherwise says no. For routing (remember, andIf) this is very simple: if every access checker answered Allowed then and only then we can answer yes. For entity access (remember, orIf) there should be no Forbidden answers and at least one Allowed to say yes.

Mar 05 2012
Mar 05

Most complex Drupal sites eventually accrue a few... oddball content types. Sometimes it's a collection of ad nodes that should only appear in a rotator, other times it's special nodes that are only used when constructing a user's profile page, and many times it's impossible to predict until the site is complete. No matter the details, it's often useful to mask the normal node/1 URLs for those "ugly duckling" content types to ensure users don't stumble onto them accidentally. Unpublishing them entirely can be a pain, though, complicating Views and permission management. Enter... Rabbit Hole.

Screenshot of administration options

Rabbit Hole is a curiously named module that lets you control the behavior of the node view pages for each content type. Pop open the administration page for each content type, scroll down, and you'll see options to leave it as is, mask it entirely and generate a 404 error, or redirect to a different Drupal path or URL. Administrators can be given permission to override these redirects, viewing the normal node pages for these content types while users see the redirect or 404.

The only downside to Rabbit Hole is its lack of support for tokens in its per-content-type redirect paths. There's no way for the node/1 path to redirect to the user page of that node/1's author, for example. Adding token support would allow tricks like that, but even without that the module is a useful addition to any site builder's toolbox. Writing the same functionality in a custom module is a common task on most large sites, but Rabbit Hole. solves it for everyone. Give it a spin!

*/
Jan 03 2009
Jan 03

I previously posted about my search for my ideal content access module. Since then I've done a lot of searching and digging through the source code of all the most popular content access modules. I've come to realize there was really no module currently out there that had the features I was looking for. A quick review of the feature wish list:

  • Per-content access restricted by user roles
  • If the content is restricted to user, don't just hide the node, but rather show a message in place of the content like "Sorry, this post has been restricted to users with the following roles: registered user, friend."
  • If the current user is anonymous and the content is restricted, also show a message like "Please create an account to view this content." with a link to the user register page, of course.

restricted_content1 The built-in Drupal node access system is not really ideal for this direction since there's absolutely no middle ground between, "This user can view the node" and "OMG! Hide this node completely!" So today I started Restricted content, a yet-another-but-totally-different node access module. After only a couple hours of coding, it seems to be working fairly well, minus any kind of testing framework (I swear I'll get right on it webchick!). The content restriction messages shown to the user are customizable and can be integrated with token for some cool token replacement. For anyone daring enough to try it or even just take a look at the code, your comments and feedback are wanted and welcome here or in the issue queue. Hopefully some of you can find this useful like I have!

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