Yii Framework Access Control Lists

January 14, 2010
The Yii Book If you like my writing on the Yii framework, you'll love "The Yii Book"!

In my series Learning the Yii Framework, I discuss the individual parts of the MVC (Model, View, Controller) architecture in some detail, from a Yii perspective. In the post on [intlink id=”659″ type=”post”]Controllers[/intlink], I introduce Access Control Lists (ACLs), Yii’s default way of restricting who can take what actions. This is a key part of the security of any Web application. For example, a site’s content can often be read by anyone at all, registered or non-registered users alike (like the text you’re reading now). Some content may only be viewable by registered users and some by registered users of a certain type (e.g., paid members). Finally, some content may only be viewable by administrators. In this post, I detail how to completely control access to your Web application using Yii’s Access Control Lists.To start by repeating what I wrote in my [intlink id=”659″ type=”post”]Basic Controller Edits[/intlink] post, a Controller’s accessRules() method dictates who can do what. In a very simple way, the “what” refers to the Controller’s action methods, like actionList() or actionDelete(). In other words, only X type of user can call the actionDelete() method, which, of course, deletes a record. Your “who” depends upon the situation, but to start there’s at least logged-in and not logged-in users, represented by * (anyone) and @ (logged-in users), accordingly. Depending upon the login system in place, you may also have levels of users. So the accessRules() method uses all this information and returns an array of values. The values are also arrays, indicating permissions (allow or deny), actions, and users:

public function accessRules()
{
    return array(
        array('allow',  // allow all users to perform 'list' and 'show' actions
            'actions'=>array('list','show'),
            'users'=>array('*'),
        ),
        array('allow', // allow authenticated user to perform 'create' and 'update' actions
            'actions'=>array('create','update'),
            'users'=>array('@'),
        ),
        array('allow', // allow admin user to perform 'admin' and 'delete' actions
            'actions'=>array('admin','delete'),
            'users'=>array('admin'),
        ),
        array('deny',  // deny all users
            'users'=>array('*'),
        ),
    );
}

That’s the default setting for a Controller, where anyone can perform list and show actions, meaning that anyone can list all records or show individual records in an associated Model. The next section allows any logged-in user to perform create and update actions. Next, only administrators can perform admin and delete actions. Finally, a global deny for all users is added, to cover any situation that wasn’t explicitly defined. This is just a good security practice. Note that these rules just apply to this Controller; each Controller needs its own rules.

So at the most basic level, your access rules begin by allowing or denying actions to logged-in or non-logged-in users. When you go to create your own rules, start with what anyone can do and end with what no one can do. For example, say you have some user management system that includes the Models User and UserType, corresponding to related tables in the database. The UserType would be a simple two-field object—id and type, where type might be reader, writer, editor, and so forth. Each User then is assigned a single, specific type. I’d be inclined to grant list and show permissions to everyone, as that might be useful for browsing users by type and would be necessary for adding new users. However, I probably wouldn’t allow create, update, admin (which is really a variation on list), or delete permissions on UserType at all, with the thinking that these are static values. The accessRules() would then be:

public function accessRules()
{
    return array(
        array('allow',  // allow all users to perform 'list' and 'show' actions
            'actions'=>array('list','show'),
            'users'=>array('*'),
        ),
        array('deny', // no one can create, update, or delete these:
            'actions'=>array('create','update','admin','delete'),
            'users'=>array('*'),
        ),
        array('deny',  // deny all users anything not specified
            'users'=>array('*'),
        ),
    );
}

As you can see, just to be extra careful, I include a final deny clause still: if actions are added later the default behavior will be denial; only by then changing the rules can that action be executed by anyone.

To take this beyond logged-in vs. non-logged-in users, you need to know that the representation of logged-in users relies upon Yii’s authentication components. In the default rules, the admin user can perform certain actions, where “admin” is the actual name of the user that is logged in, and comes from protected/components/UserIdentity.php.  I write about the authentication process in detail in two posts: the [intlink id=”826″ type=”post”]first covering simple authentication[/intlink], [intlink id=”849″ type=”post”]the second covering more evolved authentication[/intlink]. The default Yii application allows for two users, with names of demo and admin, but you’d likely have a more elaborate system in place. If you know only a limited number of users will be admins, you could hardcode their usernames into the rules:

array('allow', // allow harold and maude user to perform 'admin' and 'delete' actions
    'actions'=>array('admin','delete'),
    'users'=>array('harold','maude'),
),

This is still a bit too static, though. Perhaps your users in the database would be registered by user type, or role, and that the permissions would be based upon these roles. In that case, you can add an expression element to the returned array in the access rules:

array('allow',
    'actions'=>array('admin','delete'),
    'users'=>array('@'),
    'expression'=>'PHP code to be evaluated'
),

Two things about this expression. First, you still need to use the users element, and you’ll probably want to still restrict this to logged-in users (most likely). Second, the expression itself should be some PHP code, quoted, that when evaluated gives a Boolean result. If the code in the expression will be true, then permission will be allowed; false, denied. Say you wanted to restrict the publish action to only those with the role of editor:

array('allow',
    'actions'=>array('publish'),
    'users'=>array('@'),
    'expression'=>'isset($user->role) && ($user->role==="editor")'
),

The $user->role value would have to be established when the user logs in, as part of the authentication process. I discuss this exact example in my [intlink id=”849″ type=”post”]second post that covers more evolved authentication[/intlink]. And I put the entire expression within quotes (it doesn’t matter whether you use single or double, so long as you don’t create a parse error). With that rule, users that aren’t logged in, or users that are logged in but have non-editor roles, won’t be able to take that action.

A final way in which I’ve enforced authorization in previous Yii projects involves a bit of a hack. I had a site where users with a certain role could create certain types of content. They could also update or delete those types of content, but only if they were the one to create that content in the first place. In other words, say a user creates an event, then their user ID gets associated with that event’s record. The update and delete actions in the EventController should only be executable if the currently-logged-in user’s ID is the same as the ownerId of the event in question. As far as I know, this cannot be addressed in the accessRules() method, because those rules are evaluated prior to the loading of any specific Model. My solution, perhaps a hack but it works, was to add the proper logic within the actions. Here’s a simplified version of how that would look within one of the actions:

public function actionDelete()
{
    $event = $this->loadEvents(); // Fetch the specific event Model.
    // Can only delete the events they created:
    if ($event->ownerId == Yii::app()->user->id) {
        $event->delete();
    } else {
        throw new CHttpException(403,'Invalid request. You are not allowed to delete this event.');
    }
}

As I said, that’s a hack but it works fine for me and is simple enough to follow (also, the actions that display events for editing and deleting get changed to only list those with a matching ownerId). A better solution would likely be to use Yii’s Role-Based Access Control (RBAC). I haven’t personally gone down that path yet as I haven’t had the need; the above knowledge has more than sufficed for the sites I’ve created thus far.

As always, thanks for reading and let me know what comments or questions you may have. Thanks, Larry