《Yii1.1 Application Development Cookbook》

Chapter 10: Security

In this chapter, we will cover:

  • Using controller filters
  • Using CHtml and CHtmlPurifier to prevent XSS
  • Preventing SQL injections
  • Preventing CSRF
  • Using RBAC

Introduction

From this chapter, you will learn how to keep your application secure according to the general web application security principle "filter input escape output". We will cover topics such as creating your own controller filters, preventing XSS, CSRF, and SQL injections, escaping output, and using role-based access control.

Using controller filters

In many cases, we need to filter the incoming data or perform some actions based on this data. For example, with custom filters, we can filter visitors by IP, force users to use HTTPS, or redirect the user to an important setting page prior to using the application. Yii has two built-in usable filters. First is CInlineFilter which allows using the controller method as a filter, and the second (the one we will focus on) is CAccessControlFilter which allows controlling access to various controller actions.

In this recipe, we will implement the following:

  • Limiting access to the controller action to authorized users only
  • Limiting access to the controller action to specified IPs
  • Limiting access to specific users
  • Limiting access for users of a browser specified; in this case, we will also show the custom message

Getting ready

  • Create a fresh application by using yiic webapp
  • Create protected/controllers/AccessController.php as follows:
<?php
class AccessController extends CController
{
    public function actionAuthOnly()
    {
        echo "Looks like you are authorized to run me.";
    }
    
    public function actionIp()
    {
        echo "Your IP is in our list. Lucky you!";
    }
    
    public function actionUser()
    {
        echo "You're the right man. Welcome!";
    }
}

How to do it...

Carry out the following steps:

  1. Applying an access filter consists of two steps. First, we need to include a filter in the controller filters method. We do this as follows:
public function filters()
{
    return array(
        'accessControl',
    );
}
  1. Then, we can describe filtering rules in the accessRules method that is used by the access control filter as follows:
public function accessRules()
{
    return array(
        array(
            'deny',
            'expression' => 'strpos($_SERVER[\'HTTP_USER_AGENT\'], \'MSIE\') !== FALSE',
            'message' => "You're using the wrong browser, sorry.",
        ), 
        array(
            'allow',
            'actions' => array('authOnly'),
            'users' => array('@'),
        ), 
        array(
            'allow',
            'actions' => array('ip'),
            'ips' => array('127.0.0.1'),
        ), 
        array(
            'allow',
            'actions' => array('user'),
            'users' => array('admin'),
        ),
        array('deny'),
    );
}
  1. Now try to run controller actions using Internet Explorer and other browsers, using both admin and demo usernames.

How it works...

We will start with limiting access to the controller action to authorized users only. Add the following code in the accessRules method:

array(
    'allow',
    'actions' => array('authOnly'),
    'users' => array('@'),
),
array('deny'),

Each array here is an access rule. You can either use allow rules or deny rules. For each rule, there are several parameters.

By default, Yii does not deny everything, so consider adding array('deny') to the end of your rules list if you need maximum security.

In our rule, we use two parameters. The first is actions parameter which takes an array of actions to which the rule will be applied. The second is the users parameter which takes an array of user IDs (ones returned by Yii::app()->user->id) to determine the users this rule applies to. In our case, we used one of the following special characters: @ means "all authenticated users", while * and ? stand for "all users" and "guest users" respectively.

Rules are executed one by one starting from the top until one matches. If nothing matches, then the action is treated as allowed.

The next task is to limit access to specific IPs. In this case, the following two access rules are involved:

array(
    'allow',
    'actions' => array('ip'),
    'ips' => array('127.0.0.1'),
),
array('deny'),

The first rule allows access to the ip action from a list of IPs specified. In our case, we are using a loopback address which always points to our own computer. Try changing it to, for example, 127.0.0.2 to see how it works when the address does not match. The second rule denies everything including all other IPs.

Next, we limit access to one specific user as follows:

array(
    'allow',
    'actions' => array('user'),
    'users' => array('admin'),
),
array('deny'),

The preceding rule allows a user with an ID equal to admin to run the user action. Therefore, if you log in as admin, it will let you in, but if you log in as demo, it will not. This is the same type of rule that we used to limit access to authorized users. The only difference is that we are using an ID instead of a wildcard. Again, the second rule involved denies everything including all other users.

Finally, we need to deny access to a specific browser. For this recipe, we are denying all versions of Internet Explorer and, in fact, some other browsers with the same user agent strings. The rule itself is put on top, so it executes first as follows:

array(
    'deny',
    'expression' => 'strpos($_SERVER[\'HTTP_USER_AGENT\'], \'MSIE\') !== FALSE',
    'message' => "You're using the wrong browser, sorry.",
),
array('deny'),

The detection technique which we are using is not very reliable, as "MSIE" is contained in many other user agent strings. For a list of possible user agent strings, you can refer to the following URL:
http://www.useragentstring.com/

In the preceding code, we use another filter rule property named expression. It takes a PHP expression as a string, as an anonymous function (in PHP 5.3), or as a valid callback. In our case, we use a string.

Using PHP 5.3, the anonymous function will look like the following:

array(
    'deny',
    'expression' => function(){
        return strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') !== FALSE;
    },
    'message' => "You're using the wrong browser, sorry.",
),

The preceding expression checks if the user agent string contains MSIE. Depending on your requirements, you can specify any PHP code. The second parameter named message is used to change a message shown to the user when the access is denied.

There's more...

In order to learn more about the access control and filters, refer to the following URLs:

  • http://www.yiiframework.com/doc/guide/en/topics.auth#access-control-filter
  • http://www.yiiframework.com/doc/guide/en/basics.controller#filter
  • http://www.yiiframework.com/doc/api/CAccessControlFilter

See also

  • The recipe named Using RBAC in this chapter

Using CHtml and CHtmlPurifier to prevent XSS

XSS stands for cross-site scripting and is a type of vulnerability which allows one to inject a client-side script (typically, JavaScript) in the page viewed by other users. Considering the power of the client-side scripting this can lead to very serious consequences such as bypassing security checks, getting another user credentials, or data leaks.

In this recipe, we will see how to prevent XSS by escaping the output with both CHtml and CHtmlPurifier.

Getting ready

  • Generate a fresh web application by using yiic webapp
  • Create protected/controllers/XssController.php as follows:
<?php
class XssController extends CController
{
    public function actionSimple()
    {
        echo 'Hello, '.$_GET['username'].'!';
    }
}

Normally, it will be used as /xss/simple?username=Alexander. However, as the main security principle "filter input, escape output" was not taken into account, malicious users will be able to use it in the following way:

/xss/simple?username=<script>alert('XSS');</script>

The preceding will result in a script execution which is shown in the following screenshot:

Note that instead of just alerting XSS, it is possible, for example, to steal page contents or perform some website-specific things such as deleting all users' data.

How to do it...

Carry out the following steps:

  1. In order to prevent the XSS alert shown in the preceding screenshot, we need to escape the data before passing it onto the browser. We do this as follows:
class XssController extends CController
{
    public function actionSimple()
    {
        echo 'Hello, '.CHtml::encode($_GET['username']).'!';
    }
}
  1. Now instead of an alert, we will get properly escaped HTML as shown in the following screenshot:
  1. Therefore, the basic rule is to always escape all dynamic data. For example, we should do the same for a link name:
echo CHtml::link(CHtml::encode($_GET['username']), array());
  1. That is it. You have a page that is free from XSS. Now what if we want to allow some HTML to pass? We cannot use CHtml::encode anymore because it will render HTML as just a code and we need the actual representation. Fortunately, there is a tool bundled with Yii that allows filtering out the malicious HTML. It is named as HTML Purifier and can be used in the following way:
public function actionHtml()
{
    $this->beginWidget('CHtmlPurifier');
    echo $_GET['html'];
    $this->endWidget();
}

Alternatively, you can use it in the following way:

public function actionHtml()
{
    $purifier=new CHtmlPurifier();
    echo $purifier->purify($_GET['html']);
}
  1. Now if we access the html action using a URL such as /xss/html?html=Hello, <strong>username</strong>!<script>alert('XSS')</script> the HTML purifier will remove the malicious part and we will get the following result:

How it works...

Internally, CHtml::encode looks like the following:

public static function encode($text)
{
    return htmlspecialchars($text,ENT_QUOTES,Yii::app()->charset);
}

So basically, we use the PHP's internal htmlspecialchars function which is pretty secure if one does not forget to pass the correct charset in the third argument.

CHtmlPurifier uses the HTML Purifier library which is the most advanced solution out there to prevent XSS inside of HTML. We have used its default configuration which is OK for most of the user-entered content. In order to learn more about how to configure it, refer to links mentioned in the Further reading subsection of this recipe.

There's more...

There are more things to know about XSS and HTML purifier, which are discussed in the following section:

XSS types

There are two main types of XSS injections, which are as follows:

  1. Non-persistent
  2. Persistent

The first type is exactly the one that we have used in the recipe and is the most common XSS type that can be found in most insecure web applications. Data passed by the user or through a URL is not stored anywhere, so the injected script will be executed only once and only for the user who entered it. Still, it is not as secure as it looks. Malicious users can include XSS in a link to another website and their core will be executed when another user will follow the link.

The second type is much more serious as the data entered by a malicious user is stored in the database and is shown to many, if not all, website users. Using this type of XSS, one can literally destroy your website by "commanding" all users to delete all data to which they have access.

Configuring the HTML purifier

The HTML purifier can be configured as follows:

$p = new CHtmlPurifier();
$p->options = array('URI.AllowedSchemes'=>array(
    'http' => true,
    'https' => true,
));
$text = $p->purify($text);

For a list of all possible keys which you can use in the options array, refer to the following URL:

http://htmlpurifier.org/live/configdoc/plain.html

HTML purifier performance

As the HTML purifier performs a lot of processing and analysis, its performance is not so good. Therefore, it is a good idea not to process text every time you are outputting it. Instead, it can be saved in a separate database field as discussed in Chapter 6, Database, Active Record, and Model Tricks, in the Applying markdown and HTML or cached.

Further reading

In order to learn more about XSS and how to deal with it, refer to the following resources:

  • http://htmlpurifier.org/docs
  • http://ha.ckers.org/xss.html
  • http://shiflett.org/blog/2007/may/character-encoding-and-xss

See also

  • The recipe named Applying markdown and HTML in Chapter 6

Preventing SQL injections

SQL injection is a type of code injection that uses vulnerability at the database level and allows executing arbitrary SQL allowing malicious users to carry out such actions as deleting data or raising their privileges.

In this recipe, we will see examples of vulnerable code and fix it.

Getting ready

  • Create a fresh application by using yiic webapp
  • Create and configure a new database
  • Execute the following SQL:
CREATE TABLE `user` (
     `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `username` varchar(100) NOT NULL,
    `password` varchar(32) NOT NULL,
    PRIMARY KEY (`id`)
);
INSERT INTO `user`(`id`,`username`,`password`) VALUES ( '1','Alex'
,'202cb962ac59075b964b07152d234b70');
INSERT INTO `user`(`id`,`username`,`password`) VALUES ( '2','Qiang
','202cb962ac59075b964b07152d234b70');
  • Generate a User model using Gii

How to do it...

  1. First, we will implement a simple action that checks if the username and password that came from a URL are correct. Create protected/controllers/ SqlController.php:
<?php
class SqlController extends CController
{
    public function actionSimple()
    {
        $userName = $_GET['username'];
        $password = md5($_GET['password']);
        $sql = "SELECT * FROM user WHERE username = '$userName'
            AND password = '$password' LIMIT 1;";
        $user = Yii::app()->db->createCommand($sql)->queryRow();
        if($user)
        {
            echo "Success";
        }
        else {
            echo "Failure";
        }
    }
}
  1. Let's try to access it using the /sql/simple?username=test&password=test URL. As we are aware of neither the username nor password, it will—as expected— print "Failure".
  1. Now try another URL: /sql/simple?username=%27+or+%271%27%3D%271%2 7%3B+--&password=whatever. This time, it lets us in though we still don't know anything about actual credentials. The decoded part of the username value looks like the following:
' or '1'='1'; --

Close the quote, so that the syntax will stay correct.

Add OR '1'='1' that makes the condition always true.

Use ; -- to end the query and comment the rest.

  1. As no escaping was done, the whole query executed was:
SELECT * FROM user WHERE username = '' or
'1'='1'; --' AND password = '008c5926ca861023c1d2a36653fd88e2'
LIMIT 1;

The best way to fix it is to use a prepared statement as follows:

public function actionPrepared()
{
    $userName = $_GET['username'];
    $password = md5($_GET['password']);
    $sql = "SELECT * FROM user WHERE username = :username
        AND password = :password LIMIT 1;";
    $command = Yii::app()->db->createCommand($sql);
    $command->bindValue('username', $userName);
    $command->bindValue('password', $password);
    $user = $command->queryRow();
    if($user)
    {
        echo "Success";
    }
    else 
    {
        echo "Failure";
    }
}
  1. Now check /sql/prepared with the same malicious parameters. This time everything went fine and we have the "Failure" message. The same principle applies to Active Record. The only difference is that AR uses other syntax:
public function actionAr()
{
    $userName = $_GET['username'];
    $password = md5($_GET['password']);
    $result = User::model()->exists("username = :username
                AND password = :password", array(
                    'username' => $userName,
                    'password' => $password,
                ));
    if($result)
    {
        echo "Success";
    }
    else {
        echo "Failure";
      }
}

In the preceding code, we used the :username and :password parameters and passed parameter values as a second argument. If we had written the preceding code by just using the first argument, it would be vulnerable:

public function actionWrongAr()
{
    $userName = $_GET['username'];
    $password = md5($_GET['password']);
    $result = User::model()->exists("username = $userName
        AND password = $password");
    if($result)
    {
        echo "Success";
    }
    else
    {
        echo "Failure";
    }
}

If used properly, prepared statements can save you from all types of SQL injections. Still there are some common problems:

  • You can bind only one value to a single parameter, so if you want to query WHERE IN(1, 2, 3, 4), you will have to create and bind four parameters.
  • Prepared statement cannot be used for table names, column names, and other keywords.

When using Active Record, the first problem can be solved by using the criteria addInCondition method as follows:

public function actionIn()
{
    $criteria = new CDbCriteria();
    $criteria->addInCondition('username', array('Qiang', 'Alex'));
    $users = User::model()->findAll($criteria);
    foreach($users as $user)
    {
        echo $user->username."<br />";
    }
}

The second problem can be solved in multiple ways. First is to rely on Active Record and PDO quoting:

public function actionColumn()
{
    $attr = $_GET['attr'];
    $value = $_GET['value'];
    
    $users = User::model()->findAllByAttributes(array($attr => $value));
    foreach($users as $user)
    {
        echo $user->username."<br />";
    }
}

The second and the most secure way is using the whitelist approach as follows:

public function actionWhitelist()
{
    $attr = $_GET['attr'];
    $value = $_GET['value'];
    $allowedAttr = array('username', 'id');
    
    if(!in_array($attr, $allowedAttr))
        throw new CException("Attribute specified is not allowed.");
        
    $users = User::model()->findAllByAttributes(array($attr => $value));
    foreach($users as $user)
    {
        echo $user->username."<br />";
    }
}

How it works...

The main goal when preventing the SQL injection is to properly filter the input. In all cases except table names, we have used prepared statements—a feature supported by most relational database servers. It allows you to build statements once and then use them multiple times and provides a safe way to bind parameter values.

In Yii, you can use prepared statements for both Active Record and DAO. When using DAO, it can be achieved by using either bindValue or bindParam. The latter is useful when we want to execute multiple queries of the same type while varying parameter values:

$command = Yii::app()->db->createCommand($sql);
$username, $password;
$command->bindParam('username', $username);
foreach($records as $record)
{
    $username = $record['username'];
    $command->execute();
}

Most Active Record methods accept either criteria or parameters. To be safe, you should use these instead of just passing the raw data in.

As for quoting table names, columns, and other keywords, you can either rely on Active Record or use the whitelist approach.

There's more...

In order to learn more about SQL injections and working with database through Yii, refer to the following URLs:

  • http://www.slideshare.net/billkarwin/sql-injection-myths-and-fallacies
  • http://www.yiiframework.com/doc/api/CDbConnection
  • http://www.yiiframework.com/doc/api/CDbCommand

See also

  • The recipe named Getting data from a database in Chapter 6
  • The recipe named Using CDbCriteria in Chapter 6

Preventing CSRF

CSRF or XSRF stands for cross-site request forgery, where a malicious user tricks the user's browser to silently perform an HTTP-request to the website when the user is logged in. An example of such an attack is inserting an invisible image tag with src pointing to http://example.com/site/logout. Even if the image tag is inserted in another website, you will be immediately logged out from example.com. Consequences of CSRF could be very serious: destroying website data, preventing all website users from logging in, exposing private data, and so on.

In this recipe, we will see how to make sure our application is CSRF-resistant.

Getting ready

Create a fresh application by using yiic webapp.

How to do it...

Let's start with some facts about CSRF:

  • As CSRF should be performed by the victim user's browser, the attacker cannot normally change HTTP headers sent. However, there were both browser and Flash plugin vulnerabilities found that were allowing to spoof headers, so we should not rely on these.
  • The attacker should pass the same parameters and values as the user would normally do.

Considering these, a good method of dealing with CSRF is passing and checking the unique token during form submissions and additionally using GET according to the HTTP specification.

Yii includes a built-in token generation and token checking. Additionally, it can automate inserting a token in HTML forms.

  1. In order to turn the anti-CSRF protection on, we should add the following to protected/config/main.php as follows:
'components'=>array(
    ...
    'request'=>array(
        'enableCsrfValidation'=>true,
    ), ...
),
  1. After configuring the application, you should use CHtml::beginForm and CHtml::endForm instead of HTML form tags:
public function actionCreate()
{
    echo CHtml::beginForm();
    echo CHtml::submitButton();
    echo CHtml::endForm();
}
  1. Yii will automatically add a hidden token field as follows:
<form action="/csrf/create" method="post">
<div style="display:none"><input type="hidden" value="e4d1021e79ac
269e8d6289043a7a8bc154d7115a" name="YII_CSRF_TOKEN" />
  1. If you save this form as HTML and try submitting it, you will get a message like the one shown in the following screenshot instead of the regular data processing:

How it works...

Internally, a part of CHtml::beginForm() looks like this:

if($request->enableCsrfValidation && !strcasecmp($method,'post'))
    $hiddens[]=self::hiddenField($request->csrfTokenName, 
        $request->getCsrfToken(), array('id'=>false));
 
if($hiddens!==array())
    $form.="\n".self::tag('div',array('style'=>'display:none'), implode("\n",$hiddens));

In the preceding code getCsrfToken() generates a unique token value and writes it to a cookie. Then, on next requests, both the cookie and POST values are compared. If they don't match, an error message is shown instead of the usual data processing.

If you need to perform a POST request but not build a form using CHtml, then you can pass a parameter with a name from Yii::app()->request->csrfTokenName and a value from Yii::app()->request->getCsrfToken().

There's more...

There are more ways to improve your application security, which are discussed in the following subsections:

Extra measures

If your application requires a very high security level, such as a bank account management system, extra measures could be taken.

First, you can turn off the "remember me" feature using protected/config/main.php as follows:

'components' => array(
    ...
    'user'=>array(
        // enable cookie-based authentication
        'allowAutoLogin'=>false,
    ),
    ...
),

Then, you can lower the session timeout as follows:

'components' => array(
    ...
    'session' => array(
        'timeout' => 200,
    ),
    ...
),

Of course, these measures will make the user experience worse, but they will add an additional level of security.

Using GET and POST properly

HTTP insists on not using GET for operations that change data or state. Sticking to this rule is a good practice. It will not prevent all types of CSRF, but at least will make some injections such as <img src= pointless.

Further reading

In order to learn more about the security in Yii, refer to the following URL:

http://www.yiiframework.com/doc/guide/en/topics.security

See also

  • The recipe named Using CHtml and CHtmlPurifier to prevent XSS in this chapter

Using RBAC

RBAC is the most powerful access control method available in Yii. It is described in the guide, but since it is rather complex and powerful, it is not so easy to understand how it actually works without getting under the hood a little.

In this recipe, we will take roles hierarchy from the definitive guide, import it, and explain what is happening internally.

Getting ready

  • Create a fresh web application by using yiic webapp
  • Create a MySQL database and configure it
  • Import SQL from framework/web/auth/schema-mysql.sql
  • Configure the authManager component in your protected/config/main.php as follows:
return array(
    'components'=>array(
    ...
        'authManager'=>array(
            'class'=>'CDbAuthManager',
            'connectionID'=>'db',
        ),
    ),
...
);
  • Add additional roles to protected/components/UserIdentity.php. The users array should look like the following:
$users=array(
    // username => password
    'demo'=>'demo',
    'admin'=>'admin',
    'readerA'=>'123',
    'authorB'=>'123',
    'editorC'=>'123',
    'adminD'=>'123',
);

How to do it...

Carry out the following steps:

  1. Create protected/controllers/RbacController.php as follows:
<?php
class RbacController extends CController
{
    public function filters()
    {
        return array(
            'accessControl',
        );
    }
    
    public function accessRules()
    {
        return array(
            array(
                'allow',
                'actions' => array('deletePost'),
                'roles' => array('deletePost'),
            ), 
            array(
                'allow',
               'actions' => array('init', 'test'),
            ),
            array('deny'),
        );
    }
 
    public function actionInit()
    {
        $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');
        
        $auth->assign('reader','readerA');
        $auth->assign('author','authorB');
        $auth->assign('editor','editorC');
        $auth->assign('admin','adminD');
        
        echo "Done.";
    }
    
    public function actionDeletePost()
    {
        echo "Post deleted.";
    }
    
    public function actionTest()
    {
        $post = new stdClass();
        $post->authID = 'authorB';
  
        echo "Current permissions:<br />";
        echo "<ul>";
        echo "<li>Create post: ".Yii::app()->user->checkAccess
            ('createPost')."</li>";
        echo "<li>Read post: ".Yii::app()->user->checkAccess
            ('readPost')."</li>";
        echo "<li>Update post: ".Yii::app()->user->checkAccess
            ('updatePost', array('post' => $post))."</li>";
        echo "<li>Delete post: ".Yii::app()->user->checkAccess
            ('deletePost')."</li>";
        echo "</ul>";
    }
}
  1. Now run init once to create the RBAC hierarchy. Then, try to log in as readerA, authorB, editorC, and adminD (password is "123") and visit test and deletePost.

How it works...

The RBAC hierarchy is a directed acyclic graph, that is, a set of nodes (authorization items) and their directed connections or edges. There are three types of nodes available: roles, tasks, and operations:

A role is the authorization item attached to the user (that is, a moderator or an admin). Operation determines if an action can be performed (that is, deleting post, editing post, and so on). A task is a group of operations (that is, manage task, and so on).

There are two ways to assign a role to a user, which are as follows:

  • By using Yii::app()->authManager->assign()
  • By configuring defaultRoles in the application configuration for the authManager component

Default roles are typically used when we need to assign a role to a huge part of the users based on some PHP expression such as Yii::app()->user->isGuest.

According to rules described in the definitive guide, it is forbidden to connect higher-level nodes to lower-level nodes. For example, connect a role to a task. The opposite is permitted, so we can connect a task to a role.

When checking access, we typically pass the name of an operation and, optionally, some parameters. Internally, Yii tries to find a way from an operation specified to the current user's role using reversed breadth-first search (http://en.wikipedia.org/wiki/Breadth- first_search). Therefore, when we want to find out if a user with Role has an access to perform Operation 4, Yii will go the following way:

Operation4 – Task3 – Task1 – Role

Each node can contain a business rule or bizRule. This business rule is a string containing some PHP code that returns either true or false. The returned value determines if we can go through the node or not.

In the end, we have either reached a role that means access is granted, or tried every possible path and failed which means access is denied.

There are two ways we can check if user can perform an operation specified:

  1. Using controller's accessRules specifying an operation, a task, or a role in the roles parameter of an access rule.
  2. Using Yii::app()->user->checkAccess().

By using the second way, we can pass some data which makes its way through the authorization hierarchy and passes to every bizRule encountered.

Now, we will get back to our example. The code of the init action uses the authManager component to create the following hierarchy:

For testing permissions, we have created two actions: test which lists CRUD permissions and deletePost which is limited through the access filter. The rule for the access filter contains the following code:

array(
    'allow',
    'actions' => array('deletePost'),
    'roles' => array('deletePost'),
),

This means that we are allowing all users who have the deletePost permission to run the deletePost action. Yii starts checking with the deletePost operation and the only way it can go is admin. This means only users with the admin role will be able to delete a post.

Besides the fact the access rule element is named roles, you can specify an RBAC hierarchy node, be it a role, task, or an operation.

When we check for the readPost permission for a user logged in as authorB, Yii checks readPost, reader, and then editor. Checking for updatePost is complex:

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

We use a second parameter to pass a post (in our case, we have simulated it with stdClass). If a user is logged in as authorB, then to get access we need to go from updatePost to author. In the lucky case, we have to go through only updatePost, updateOwnPost, and author. As updateOwnPost has a bizRule defined, it will be run with a parameter passed to checkAccess. If the result is true, then access will be granted. As Yii does not know what the shortest way is, it tries to check all possibilities until either there is success or no possible ways left.

There's more...

There are some useful tricks that will help you to use RBAC efficiently, which are discussed in the following subsections:

Naming RBAC nodes

A complex hierarchy becomes difficult to understand without using some kind of a naming convention. One possible convention that helps not to get us confused is as follows:

[group_][own_]entity_action

Where own is used when the rule determines an ability to modify an element only if the current user is the owner of the element and group is just a namespace. Entity is a name of the entity we are working with and action is the action that we are performing.

For example, if we need to create a rule that determines if the user can delete a blog post, we will name it as blog_post_delete. If the rule determines if a user can edit the own blog comment, the name will be blog_own_comment_edit.

A way to keep the hierarchy simple and efficient

Follow these recommendations when possible to maximize the performance and reduce hierarchy complexity:

  • Avoid attaching multiple roles to a single user.
  • Don't connect nodes of the same type. So, for example, avoid connecting one task to another one.
Avoiding RBAC

In order to keep the hierarchy even simpler, we can avoid creating and using additional nodes in some cases by replacing them with additional conditions. A good example is the editing of Post. We can create a blog_own_post_edit node with bizRule as follows:

return Yii::app()->user->id==$params["post"]->author_id;

Alternatively, we can add the same logic to the post selection routine as follows:

$post = Post::model()->findByAttributes(array(
    'id' => $id,
    'author_id' => Yii::app()->user->id,
));
if(!$post)
    throw new CHttpException(404);

By using the second way, we will avoid getting an RBAC hierarchy node from storage and traversing it.

Further reading

In order to learn more about role-based access control, refer to the following resources:

  • http://www.yiiframework.com/doc/guide/en/topics.auth#role-based-access-control
  • http://en.wikipedia.org/wiki/Role-based_access_control
  • http://en.wikipedia.org/wiki/Directed_acyclic_graph

See also

  • The recipe named Using controller filters in this chapter
评论 X

      友荐云推荐
      Copyright 2011-2014. YiiBook.com