Yii Framework Access Control Lists

January 14, 2010 — 72 Comments
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 Controllers, 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 Basic Controller Edits 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 first covering simple authentication, the second covering more evolved authentication. 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 second post that covers more evolved authentication. 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

If you enjoyed this post, then please consider following me using your favorite social media, the RSS feed, and/or by subscribing to my newsletter. Or go crazy, and buy one or more of my books . Thanks!

72 responses to Yii Framework Access Control Lists

  1. Hi Larry,

    Thanx for all your Yii posts so far, they helped me a lot. I just figured out the same yesterday about ACL’s, and I came up with this in my accessRules method, pretty the same but a bit more readable:


    ‘expression’=>’User::hasRole(User::ROLE_ADMIN)’,

    Anyway, thanks again and keep up the good posting…

    • Thanks for the nice words and for sharing your work. I assume, then, that the User Model has the ability to retrieve the currently-logged-in user and that the hasRole() method returns a Boolean value.

  2. Oh, and about your actionDelete access checks, I think is ok to do it that way. Yesterday I read all about the CAuthManager components, but that ends up as pretty heavy to implement and even then you end up with a thing like this:

    if(Yii::app()->user->checkAccess(‘updatePost’))
    {
    // update post
    }

    • Thanks again for sharing. If that’s their solution, it’s pretty close to what I end up doing anyway.

    • I think you often needs to check whether the logged in user is the owner of a post.
      And when you only need this as role management it is better do it without rbac because you must do the same then mindhout says.
      And when you have for example a task updateOwnPost and check only this there are 3 queries needed. When you check only for an operation like update then there are needed 6 or seven queries to check it.
      Read the rbac docs.
      For simple said I agree with Larry. When you not need a complex role system wiht many of roles tasks and operations it is better to do this wiht ACL.

      • Thanks for the input, Klaus.

      • I think the only difference between Larry’s hack and the RABC’s method is the fact that Larry’s auth testing can be seen in the code by other programmers while RABC’s method is hidden in a separate file. If you only need to use it once, then Larry’s method is my first choice. But, if you need to use it in multiple locations within the project, then RABC’s method will be better (in case you need to change/update the testing condition like using username instead of id)

  3. Thanks for the tip, I’ve problems with yii 1.1 accessRules.

  4. In your second code snippet you have the line

    array(‘allow’, // no one can create, update, or delete these:

    Shouldn’t that read ‘deny’ instead of ‘allow’?

  5. Very helpful, exactly what I was looking for – thanks a lot!

  6. Thanks. I spent two days in Yii RBAC, sRBAC and what nots. But, finally your method works like a charm and is much easier and faster.

  7. Hey Larry,

    I just want to say thank you. I’m starting out in yii and your site is a better resource for me than the yii documentation.

    IGerard

  8. complicated.. thx 4 share

  9. thank you so much for this post,.

  10. Hi Larry,

    My situation is the same as Gerard above. Your writings have been far more useful. Thank you very much. Keep up the good work. You are really talented in it.

    Maurice
    shalomsoftware.com.au

  11. While searching the web in trying to understand how to use Yii RBAC (for some time) your wonderful simplification that actually works popped up in the search results. Thank you so much!

  12. Thanks Larry! that put me right into what i wanted to do and explained a lot. Cheers from Poland!

  13. Hi larry , i have this code and don´t filter good the users

    return array(
    array(‘allow’, // allow all users to perform ‘index’ and ‘view’ actions
    ‘actions’=>array(‘index’),
    ‘users’=>array(‘*’),
    ),
    array(‘allow’, // allow authenticated user to perform ‘create’ and ‘update’ actions
    ‘actions’=>array(‘update’,’view’),
    ‘expression’=>'(Yii::app()->user->getState(“idTipo”)==1)’
    ),
    array(‘allow’, // allow admin user to perform ‘admin’ and ‘delete’ actions
    ‘actions’=>array(‘admin’,’delete’,’create’),
    ‘expression’=>'(Yii::app()->user->getState(“idTipo”)==2)’,

    ),
    array(‘deny’, // deny all users
    ‘users’=>array(‘?’),
    ),
    );

  14. @Larry thanks, Larry.
    this post solved my problem.thanks.

  15. Hi Larry, am using yii_1.1.7. but when i tried to edit UserIdentity.php in order to enable users from the DB to login, it gives me Incorrect username or password error whenever am loging in. I’ve tried this for three days and have failed what can i do?

  16. Just want to say, your are great write. I have learned many thing from your write ups. I have only one questions which is still confusing and i don’t know how to make that efficiently and that is how to make the admin panel and main website separate and how can i make the theme only for admin panel?

    • Thanks for the nice words on the writing. In answer to your question, you’ll want to create a “module”, which is like a separate Yii application.

  17. Being an old Fusebox, CodeIgniter, and Zend Framework guy I was ready to learn something new. Picked up Yii and have been working on a pet project with it. I’ve read the Agile Yii book and what’s so far been released of the Yii 1.1 RAW cookbook. Learned a lot but still trying to get the feel. I must say though, what a treat it was to find your site. Your articles are so concise and spot on. Not to mention the fantastic prose! Thank you so much for having to passion to take the time and share with the rest of us.

  18. Thanks Larry this has really helped me.

  19. Larry just a question because of some minor confusions I have controller with an actionAdmin so in my app I allow all authenticated users to do admin tasks but load only the items that belongs to that user. I have tried to right a code within the actionAdmin to load the users item based on the logged in user id but does work. how can i go about by modifying yii’s default actionAdmin.

    • The short answer is you just edit the method. The long answer is I don’t understand exactly what you mean and I’d prefer that questions like this, which would require longer replies and more replies and more replies and code not be done through the comments to a blog post. Please use my support forum or the Yii support forum if you need help.

  20. Very nice post! Helped me a lot! :) THANKS!

    It is much easier to understand than the yii role-based-acces-control article on yiiframework.com …
    I already spent a lot of time on the rbac tutorial… and I still have some understanding problems.
    I read your post only one time and understood everything.

    But I have a little question:
    What is the “advantage” of using the “Yii-RBAC” instead of your solution?
    Your solution is much easier, less work… and it does the same thing… (or not?)

    Best Regards from germany! :)

    • Thanks for the nice words. Glad you liked it. As for your question, well, my solution is a bit of a hack, as I said, and hacking framework (i.e., standardized) code isn’t really a good thing. And the RBAC allows finer control. I’m not saying you should rush out to use RBAC, but it has its merits, of course.

  21. Hello,
    Regarding you last example (checking if the event being deleted belongs to the user), what if the relationship is MANY_MANY, that is, I don’t have a “ownerId” but an array of owners?

    I could I guess iterate over the values of the array but I’m wondering if there is a Yii-way of doing this.

  22. Hi Larry! Thanx again for the great post! Just to get everything ordered in future – can you explain why somewhere is used === instead of == and :: instead of -> ?…

    • Hello Alex. === is an identical comparison; == is an equality comparison. It’s best to make identical comparisons in situations where you want to distinguish between true-like values and actual true, or false-like values (such as 0) and actual false. :: is the scope resolution operator, used to refer to a member of a class. -> is for referring to a member of an object. None of these are particular to frameworks; they’re part of PHP proper.

  23. Larry, your tutorial series on Yii is really a great resource for the community, and more so for people just beginning on Yii, since these are specific use-case based deep dives.

    Wondering if your method would be a good natural fit to where several thousand (maybe couple of hundred thousand) users, each having their own “private” content. E.g.

    user0001 has access to content0001a, content0001b, content0001c…
    user0002 has access to content0002a, content0002b, content0002c…

    user000N has access to content000Na, content000Nb, content000Nc…

    although all users, do have access to some limited shared / common content as well. The content in question is of sensitive nature (e.g. scanned tax filing images etc.), so the ACL needs to be pretty much bullet proof.

    Would the ACL method scale well as per this use case ?

    • It sounds like going the more formal route of an RBAC would seem to make the most sense. I think of ACL as being a relatively simple approach and the problem is it needs to be hardcoded into each controller, which would be unmanageable in your case, I suspect. Thanks for the nice words!

    • As suggested in another comment, why not do the access checking in the model itself?
      ACL in powerful implementations supports hierarchies/grouping. That means you can create a group for every user, to which you assign all his private content. Then you grant the user access to this specific group. For the public content, you also create a group and grant all users permissions on this group, respectively.

      As you use groups instead of assigning the permissions for each object this should scale quite well.

      Regards,

      • The issue with putting the access checking in the model is that it makes it very hard to do unit testing and integration testing as you need a valid user object and you have to do some nasty workarounds with Yii in order to have your tests passing correctly.

        • That’s not a problem as you can ignore the access checking (with a global param for instance) if you are in debug mode. It’s a good idea to use a component or a dedicated behavior for that anyway – that could take care of that globally.

  24. This is a great post. I after looking more into ACLs, I found that RBAC was more for my needs. I created a blog post on my discoveries that might help out a few others if they decide to go down that path.

    Yii and Authorization Rules (RBAC)

  25. Whenever the Yii documentation gives me a headache your site is where I go. The “about box” on your site may seem a bit tacky at times but it’s so very true. “Translating geek into English” is your thing. Your Yii book will probably happen after my dissertation deadline but even your blog posts are pure gold. In this case the access rule expression with example solved my problem. So many times others fail to show the last step to make things actually work. I’m glad that you don’t

    Thanks for taking the time to share your knowledge!

  26. Larry thank you very much, your mini-guide YII is very good.

    :)

  27. This post has enlighten me about the access control. Is it possible to implement access control for specific module? not only in controller. Thanks

    • Off the top of my head, I don’t know, but that’s a good question. I’ll see if I can figure that out in time.

    • Yii doen’t care how your auth items are called nor what they actually are. Feel free to create extra auth items for your modules. However, you will need to a) do the check in each module separately (you could use a common base class for this) or b) you use custom routing

      Regards,

  28. There’s a difference between RBAC and ACL, and just to put it clearly, Yii uses RBAC and not ACL. For the differences between both approaches just take a glance at the wikipedia pages.

    Regards,

    • Yes, there’s clearly a difference between RBAC and ACL. In fact, they even have different names. But, no, Yii uses access control by default, and RBAC if you enable it. This is in the Yii docs.

      • Yes. As you can read in the docs, you read nothing about ACL but you find a link on in what RBAC differs from other strategies. In Yii, whenever you check permissions, you check if you can do something on a certain type of objects. You don’t care which object you have, it’s just an object. In fact, you even _cannot_ define fine-grained permissions on specific objects to specific objects (ok you could abuse the system, but that’s not how it’s supposed to be).
        ACL is a more general approach allowing you to use assignments between various objects – real objects and abstract ones (the ones Yii uses). Yii does not implement ACL in it’s powerful extent. Yii does implement RBAC but only a (small) subset of ACL.

        Regards,

  29. Hello Larry, again thanks for another great post on Yii. I am reading special Topic in Yii’s RBAC.
    It look like Yii’s RBAC rule is fixed and i need to hard code those Access Checking (eg. Yii::app()->user->checkAccess(‘createPost’)) in controller. Is there any technique/way to alter the permission dynamically without rewriting the rule?

    • Just do the check in your model: Post::beforeSave() (and return the result)
      If it doesn’t succeed, the Post will be prevented from being saved. This is independent of any controllers and you have the check in _one_ file, so if you want to alter the permission later on, it’s just one line.

      Regards,

  30. Guruprabakaran July 25, 2012 at 4:01 am

    Thanks for the valuable script

    Very very thanks

    expecting more do well

    Thanks

  31. Hearty Thank you very much for this code

    array(‘allow’,
    ‘actions’=>array(‘publish’),
    ‘users’=>array(‘@’),
    ‘expression’=>’isset($user->role) && ($user->role===”editor”)’
    ),

  32. Why one would not use the expression to call a function that checks if the user is the owner of the model?
    It seems a better administrative approach to see all the access conditions in one place (in the accessRules). And the action to do if denied, would be handle in one place (no need to throw new CHttpException).

    Here is my example for a controler:

    public function accessRules()
    {
    return array(‘allow’,
    ‘actions’=>array(‘delete’),
    ‘users’=>array(‘@’),
    ‘expression’=>’yii::app()->controler->accessOwnerOnly()’
    ), …
    }

    public function accessOwnerOnly()
    {
    if ( Yii::app()->user->id == $this->loadModel( $_GET[‘id’] )->ownerId )
    return true;
    return false;
    }

    I’d also take care of caching the loadModel result to avoid multiple sql queries. And I’d use/define a function getOwnerUserId in the model rather than just calling ownerId.

    But I’m new to yii and I might miss something important, any feedback / idea?

  33. I have read three of your posts, and all of them helped.

    Thank you!

  34. Hi Larry, thanks for the post. It was very helpful. I also came across the ACL extension for yii. How exactly it is different from what I have seen above in your post. I think the functionality explained above solves the access issues pretty much. What’s your views?

Trackbacks and Pingbacks:

  1. Larry Ullman's Blog » Forcing Login for All Pages in Yii - July 20, 2010

    […] time back, I had written a couple of blog posts on authentication and authorization in Yii. As a comment to one of those posts, someone shared some code (also posted in the Yii […]

  2. RBAC it up! - May 23, 2012

    […] the ins and outs of Yii authentication from an excellent collection of posts by Larry Ullman.  Need to take it to the next level with some […]

Comments are great, but I'd strongly prefer any requests for assistance get made in the support forums. Thanks!