Difference between #5 and #4 of Getting to Understand Hierarchical RBAC Scheme

unchanged
Title
Getting to Understand Hierarchical RBAC Scheme
unchanged
Category
Tutorials
changed
Tags
hierarchy, RBAC, securitysecurity, understanding
unchanged
Content
<a
href="http://www.yiiframework.com/doc/guide/1.1/en/topics.auth">Authentication
and Authorization</a> is a good tutorial. Among other topics, it describes
basic aspects of Yii's RBAC implementation. But however hard I read the
tutorial, I couldn't understand <i>how exactly</i> the
hierarchy works. I found how to define authorization hierarchy, how business
rules are evaluated, how to configure authManager, but almost nothing about how
I should build my hierarchy, in what sequence its nodes are checked, when the
checking process stops and what would be the checking result.

There was no other way for me but to dig into Yii's code and I would like
to present my findings in this article. I have to mention that digging in
Yii's code is not difficult at all, it's well-structured and everyone
can do it, but the following info can save you a bit of time particularly when
you're a Yii newbie.

I must say it would be much easier for you to understand the article if you got
familiar with the above-mentioned tutorial especially with the topics starting
from <a
href="http://www.yiiframework.com/doc/guide/1.1/en/topics.auth#role-based-access-control">Role-Based
Access Control</a>.

Let's consider the hierarchy example from the tutorial (this example
illustrates how security can be built for some blog system):

~~~
[php]
$auth=Yii::app()->authManager;
 
$auth->createOperation('createPost','create a post');
$auth->createOperation('readPost','read a post');
$auth->createOperation('updatePost','update a post');
$auth->createOperation('deletePost','delete a post');
 
$bizRule='return
Yii::app()->user->id==$params["post"]->authID;';
$task=$auth->createTask('updateOwnPost','update a post by
author himself',$bizRule);
$task->addChild('updatePost');
 
$role=$auth->createRole('reader');
$role->addChild('readPost');
 
$role=$auth->createRole('author');
$role->addChild('reader');
$role->addChild('createPost');
$role->addChild('updateOwnPost');
 
$role=$auth->createRole('editor');
$role->addChild('reader');
$role->addChild('updatePost');
 
$role=$auth->createRole('admin');
$role->addChild('editor');
$role->addChild('author');
$role->addChild('deletePost');
~~~

First of all I'd like to convert this to a more human-readable form:

<table align="center" cellpadding="0"
cellspacing="0" class="tr-caption-container"
style="margin-left: auto; margin-right: auto; text-align:
center;"><tbody>
<tr><td style="text-align: center;"><a
href="http://i55.tinypic.com/2yk0wzk.png" imageanchor="1"
style="margin-left: auto; margin-right: auto;"><img
border="0" src="http://i55.tinypic.com/2yk0wzk.png"
/></a></td></tr>
<tr><td class="tr-caption" style="text-align:
center;">Sample blog system authorization
hierarchy</td></tr>
</tbody></table>
The <span style="background-color:
#9fc5e8;">turquoise</span> boxes represent roles, the <span
style="background-color: #ffd966;">yellow</span> box is a
task, and the most fine-grained level of the authorization hierarchy -
operations - are <span style="background-color:
#f9cb9c;">tan</span>. Collectively roles, tasks and operations are
called <b>authorization items</b>. You should keep in mind that
functionally all auth item types are equal. It's completely up to you to
make some auth item a role or a task - still it would do the same thing.
Different types of auth items are introduced solely for the purpose of naming
convenience. You are not limited to the three authorization levels: there can be
multiple levels of roles, tasks and operations. (Getting back to our diagram,
you can see this point illustrated by multiple levels of roles.) Also you may
skip any of these levels (the role <b>author</b> has immediate child
operation <b>create</b>). The only restriction is that in the auth
hierarchy roles should stay higher than tasks and tasks should stay higher than
operations.

Now let's take a quick look at what was on blog system creator's mind.
Everything seems to be quite logical. The weakest role is
<b>reader</b>: the only thing he is allowed to do is to
<b>read</b>. An <b>author</b> has a bit more power: he
also can <b>create</b> posts and <b>update his own</b>
posts. <b>Editors</b> can read posts and <b>update</b>
(edit) <i>all</i> posts, not own ones (in fact, according to the
hierarchy, editors can't create posts and that's why editors
haven't got any <i>own posts</i> at all). And of course, the
most powerful role is <b>admin</b> which can do anything.

If you are familiar with the principles of object-oriented hierarchy, your
former knowledge may lead you to a confusion. In every subsequent level of an
object tree, objects obtain (inherit) all (or part) of the features of their
parent (base) objects. This results in that bottommost objects are most
"loaded" with features, while the root objects have only basic
features. The opposite happens with RBAC hierarchy in Yii. The bottommost items
in the authorization hierarchy represent basic operations, while the topmost
authorization items (usually roles) are the most powerful and compound ones in
the whole authorization system.

So now that the idea behind the hierarchy is clear, let's understand how
the access checking works. To check if the current user as allowed to perform a
particular action, you should call the the <b>checkAccess</b>
method, for example:

~~~
[php]
if(Yii::app()->user->checkAccess('createPost'))
{
    // create post
}
~~~

How our hierarchy is used by Yii to check the access? Although you are not
required to read this to understand the rest of the article, I provide here an
example piece of Yii's code responsible for access checking (an
implementation of CAuthManager for databases - CDbAutManager) for your
reference:

~~~
[php]
if(($item=$this->getAuthItem($itemName))===null)
 return false;
Yii::trace('Checking permission
"'.$item->getName().'"','system.web.auth.CDbAuthManager');
if($this->executeBizRule($item->getBizRule(),$params,$item->getData()))
{
 if(in_array($itemName,$this->defaultRoles))
  return true;
 if(isset($assignments[$itemName]))
 {
  $assignment=$assignments[$itemName];
 
if($this->executeBizRule($assignment->getBizRule(),$params,$assignment->getData()))
   return true;
 }
 $sql="SELECT parent FROM {$this->itemChildTable} WHERE
child=:name";
 foreach($this->db->createCommand($sql)->bindValue(':name',$itemName)->queryColumn()
as $parent)
 {
  if($this->checkAccessRecursive($parent,$userId,$params,$assignments))
   return true;
 }
}
return false;
~~~

When you call <b>checkAccess</b>, Yii begins to recursively climb
along the authorization hierarchy and check each item's business rule. For
instance, when you make a call like this 

~~~
[php]
Yii::app()->user->checkAccess('readPost')
~~~

Yii first checks the <b>readPost</b>'s business rule (recall
that an empty business rule is equivalent to a business rule always returning
<b>true</b>). Then it searches for all
<b>readPost</b>'s parents - these are <b>author</b>
and <b>editor</b> - and checks their business rules as well. The
process doesn't stop when a business rule has been evaluated to
<b>true</b>; it only stops when some rule returned
<b>false</b> or we have reached the top of the hierarchy and there
are no more parents to check.

So what are ways for the <b>checkAccess</b> method to return
<b>true</b>? They are two. First, the iteration can stop with a
positive result when Yii encounters in the hierarchy a so-called
<b>default role</b> - a role that is assigned by default to all
authenticated users. For our blog system this can be the
<b>reader</b> role. Default roles can be set up in the web app
configuration file; how this is done is described thoroughly in the <a
href="http://www.yiiframework.com/doc/guide/1.1/en/topics.auth#using-business-rules">Using
Default Roles</a> section of the tutorial.

The second way to make <b>checkAccess</b> return
<b>true</b> is explicitly creating an <b>authorization
assignment</b> which is basically defining an &lt;auth
item&gt;-&lt;user&gt; pair. In code, this can be done like this:

~~~
[php]
$auth->assign('reader','Pete');
$auth->assign('author','Bob');
$auth->assign('editor','Alice');
$auth->assign('admin','John');
~~~

which is semantically equivalent to assigning roles to users. You're not
limited to assigning roles; individual tasks and operations can be assigned to
users as well. In real life, it is more practical not to hard code all auth
assignments but to store them in a database. You can implement this scenario
using the <b>CDbAuthManager</b> component which is described in the
Yii tutorial.

Let's get back to the <b>checkAccess</b> discussion. Before
hierarchy iteration begins, Yii collects all authorization items assigned to the
current user and at each iteration step checks if current hierarchy's auth
item is in the assignment list. If it is, the iteration stops and returns a
positive result.

Assume we are implementing security for the "update post" user action.
Whoever is logged in into our blog system should pass our authorization check
before he is able to edit a post. Therefore the most appropriate place to check
the access is the beginning of the respective controller action:

~~~
[php]
public function actionUpdatePost()
{
 if(!Yii::app()->user->checkAccess('updatePost'))
  Yii::app()->end();

 // ... more code
}
~~~

Suppose the current user is <b>Alice</b>. Let's see how Yii
processes the auth hierarchy. Although <b>updateOwnPost</b> is an
immediate parent of <b>updatePost</b> and returns
<b>false</b>, Yii quickly finds another parent auth item which
returns <b>true</b> - the <b>editor</b> role. As a
result, Alice gets a permission to do a post update. What happens if
<b>Bob</b> logs in? In this case the branch of the hierarchy going
through the <b>editor</b> item is also processed but no item along
it returns <b>true</b>. The only possible way for the access check
to succeed is then to go through the <b>updateOwnPost</b> item. 

But instead of an empty "always-true" business rule
<b>updateOwnPost</b> has a more complex one (see the first code
snippet at the beginning of the article) and for the evaluation it requires the
post creator's ID. How can we supply it to the business rule? In the form
of <b>checkAccess</b>'s parameter. To achieve this we need to
modify our controller action handler in the following way:

~~~
[php]
public function actionUpdatePost()
{
 // here we obtain $post, probably via active record ...

 if(!Yii::app()->user->checkAccess('updatePost',
array('post'=>$post)))
  Yii::app()->end();

 // ... more code
}
~~~

Note that despite <b>updateOwnPost</b> returns
<b>true</b> for <b>Bob</b>, the iteration through auth
hierarchy still goes on. It only stops and returns success when it reaches the
<b>author</b> item.

I think now you're able to figure out how Yii would check access given that
<b>Pete</b> or <b>John</b> logged in.

Returning to the above code snippet, it may seem that we're providing the
<b>post</b> parameter to the <b>updatePost</b> operation
whose business rule is empty and requires no parameters at all. This is truth
but not all of it. In fact Yii passes the same parameter set (there can be
several parameters as they are passed as an array) to every hierarchy item at
every iteration. If item's business rule requires no parameters, it simply
ignores them. If it does require them, it takes only those that it needs.

This leads to the two possible parameter passing strategies. The first one is to
remember for every auth item what other auth items can be reached from it in the
hierarchy and provide each call to <b>checkAccess</b> with the exact
number of parameters. The advantage of this strategy is code brevity and
probably efficiency. The other strategy is to always pass all parameters to
every auth item, no matter if they would actually be used for business rule
evaluation. This is a "fire-and-forget" method which can help to avoid
much of trial and error while implementing you app's security. Its downside
is possible code clutter and maybe drop in script performance.

This is only basic information about the RBAC authorization model in Yii; much
more advanced security models can be built using it. Please refer to <a
href="http://www.yiiframework.com/doc/guide/">The Definitive Guide
to Yii</a> and <a
href="http://www.yiiframework.com/doc/api/">Class
Reference</a> for more details. Also there's a number of web
interfaces implemented as <a
href="http://www.yiiframework.com/extensions/?category=1">extensions</a>
which can help you do the Yii RBAC administration.