RBAC is a simple and powerful way to centrally control access to a web application. Its main advantage is that with the right understanding and application of the authorization hierarchy, you can very flexibly control access without changing the code of the controllers.
Unfortunately, the standard RBAC manual in YII leaves more questions than answers. I intend to correct this situation.
I will talk about creating the “right” hierarchy: how to do it is not worth it. And in the end, I saved the instruction on how to make LDAP authorization (from ActiveDirectory) friends with Yii and RBAC.
All who are interested, welcome under the cat!
')
RBAC
(Role Based Access Control) Role-based access system. The basis of this system in Yii consists of three main links:
- Role
- Task (Task)
- Operation (Operaton)
I assume that the reader has already briefly familiarized himself with the
page of the official textbook YII and knows the basic principles of the authorization mechanism in Yii.
Therefore, we proceed immediately to building the correct hierarchy of authorization elements.
Role hierarchy
The most important, and the most confusing, is the hierarchy of elements in RBAC. It depends on how well it is thought out how flexible you can manage roles in the system, and how often you will have to change the controller code.
Let's take a closer look at each authorization element:
- The operation is the lowest element of authorization. That is what we should check in the code of our controllers. In other words: an operation is something that clings to code.
- Role is the opposite, the highest element of authorization, which groups in itself operations and tasks. And that is the role we should attach to users.
- Task - This is an optional element between the operation and the role, and narrows the rights of the operation using bizRule. To facilitate understanding, let's call it a filter .

The diagram above shows a typical hierarchy, where controllers check operations, and roles are attached to users. However, YII does not prevent you from checking something else in the controller, for example, the user's role.
But you must remember that this is WRONG and leads to the fact that you lose the benefits of centralized management.
Consider an example:
We have news, to the management of which we want to differentiate access.
The first thing we have to do when designing RBAC is to consider possible OPERATIONS (and not user roles, as it seems at first glance).
News can usually be
deleted ,
created ,
read and
edited .
Let's transform these actions into operations:
deleteNews ,
createNews ,
readNews ,
updateNews .
In the code, you can check any of these operations:
if(Yii::app()->user->checkAccess('createNews')) {
After the operations are thought out, you can move on to the roles (I deliberately skip tasks, we'll talk about them a bit later):
From the available operations we can distinguish the following roles:
newsReader ,
newsManager ,
newsAuthor .
The hierarchy of elements will be as follows:
- newsReader
- newsAuthor
- newsManager
- readNews
- createNews
- deleteNews
- updateNews
These roles can be attached to specific users. But it is better to create one more abstraction of roles more generalized, and attach it to users, for example:
Such an abstraction is convenient if we have to manage not only the news, but also, say, pictures in the photo gallery or goods in the store. Then for each such section of your system you need to create your “intermediate” roles, for example,
photoReader (showPhoto) ,
photographer (showPhoto, addPhoto) ,
photoManager (showPhoto, addPhoto, deletePhoto) and attach them to generic roles:
Those. it can be read as: a guest can read the news and watch a photo; authorized user can write news and add photos; the moderator can do all of the above, as well as edit and delete someone else's photos and news.
You have probably noticed that the update operation is not available for the newsAuthor and the photographer roles. That's right, because if at this stage we give them the updateNews or updatePhoto operations, then they will be able to manage all the photos indiscriminately. And we need that the authors could edit only their elements.
That is what created the problem. Tasks are filters that can clarify rights. Create an
updateOwnNews task, with a descendant of which we assign
updateNews . From the title of the task it is clear that it will allow you to edit your own news, and bizRule will help us in this.
bizRule is a certain PHP code, the result of which should be the answer: whether to apply this rule to this user or not.
The bizRule for our updateOwnNews task will look like this:
$bizRule='return Yii::app()->user->id==$params["news"]->authID;';
Where we check if the news author’s id is the same as the current authorized user.
To get the current news item in a business rule, you must first pass it there:
$params=array('news'=>$post); if(Yii::app()->user->checkAccess('updateNews',$params)) {
Note that we are not checking for a specific task (
updateOwnNews ), but for the
updateNews operation (the lowest element of the hierarchy).
Thanks to the hierarchy, which after the creation of the updateOwnNews task has become such:
- newsAuthor
- readNews
- createNews
- updateOwnNews
Yii will start the access check from the bottom and go up the hierarchy, i.e. will check updateNews, then go to updateOwnNews. At each stage of the check, Yii will check whether the bizRule is set, and if set, it will pass to it the parameters specified in the checkAccess function.
Schematically, the test can be represented as:

The diagram shows 3 verification scripts:
The first scenario is when an authorized user tries to edit their news. In this case, checking up from the bottom up goes through
updateOwnNews . And since the user IDs are the same, they succeed.
In the second case, the user has the role of moderator. There is no
updateOwnNews task in its hierarchy, so only the existence of the updateNews operation is checked.
Verification is successful.
In the third case, an authorized user tries to edit someone else's article, and at the
updateOwnNews stage
, the check fails, because not satisfied bizRule task.
The examples above demonstrate centralized rights management.
Having written a check on the execution of the operation once in the controller and passing the parameter there, all further work on access control falls on RBAC.
Therefore, if possible, you should
always pass parameters to the
checkAccess function (even if the element being checked does not have a bizRule), and check the operation, not the role.
If the controller in one condition you want to write more than one check - you know, you are going in the wrong direction - you have problems with the organization of the hierarchy.
For example:
if(Yii:app()->user->checkAccess('moderator') && Yii:app()->user->checkAccess('administrator')) {
It is not right. With this approach, you can not centrally manage rights. Each time you have to edit the code and add a new condition here.
Ways to verify rights in controllers
There are 2 ways to verify rights.
The first method we have already considered. This is the
checkAccess () method of the
CWebUser component.
But for those who care that their controllers are not “plumper”, there is another
aspect-oriented way to verify rights.
The method is to connect to the controller filter 'accessControl'.
This filter will do all the dirty work for you: it will check for the access rights and, if necessary, send the user to page 403. Thus, you will not have to duplicate the code of checks in each action.
Consider the filter operation on the example of the news controller:
class NewsController extends CController { … public function filters() { return array( 'accessControl', ); } public function accessRules() { return array( array('allow', 'actions'=>array('create'), 'roles'=>array('createNews'), ), array('allow', 'actions'=>array('delete'), 'roles'=>array('deleteNews'), ), array('allow', 'actions'=>array('view'), 'roles'=>array('readNews'), ), array('allow', 'actions'=>array('update'), 'roles'=>array('updateNews'), ), ); } ... }
In the
accessRules function
, we specify 4 rules, each of which is an array. Where the
actions key indicates for which Actions we apply the rule, and the
roles key for which roles.
It should be noted that in spite of the fact that the key is called 'roles', any element of authorization can be entered there, be it an operation or a task. But as we all know, in the controllers we need to check
only the operations, therefore, they are written in the example above.
This filter helps us get rid of many lines of pass-through code in actions. However, there is a problem: we need to transfer the current news to 'updateNews', so that the bizRule defined in updateOwnNews will work correctly.
To understand how you can pass parameters to the filter, I had to get into the framework code and peep there. Fortunately, with version 1.1.11 this possibility has appeared.
To pass parameters, you need to write a rule like this:
'roles'=>array('newsUpdate'=>array('news'=>$news))
However, the problems do not end there. The filter is launched BEFORE any action, which means we have not yet created a news item that we could transmit.
The solution may be the following approach:
protected $model; public function accessRules() { return array( ... array('allow', 'actions' => array('update'), 'roles' => array( 'updateNews' => array( 'news' => $this->news )), ), ... ); } public function getNews() { if ($this->actionParams['id']) { return $this->loadModel($this->actionParams['id']); } } public function loadModel($id) { if ($this->model === null) $this->model = News::model()->findByPk($id); if ($this->model === null) throw new CHttpException(404, 'The requested page does not exist.'); return $this->model; }
Here I create a getter for the news field in which I get the news model via the loadModel function. But in order not to pull the base several times (at the beginning when checking the rights, and then in the action itself), I created a closed $ model field into which I cached the model. And the next time the loadModel function is called, the model will be obtained from the property, and not from the base.
Unfortunately, this method is not suitable if you need to send parameters to the rule that require more complex logic. Therefore, it remains to use checkAccess () for such cases;
RBAC Yii and LDAP
LDAP is the Lightweight Directory Access Protocol, the “lightweight directory access protocol,” Wikipedia tells us. In our case, we will get access to the ActiveDirectory directory, in order to authorize users from the corporate network using their login and password.
PHP has built-in support for LDAP, and therefore you don’t have to invent anything, and there are also many ready-made components that provide a convenient interface for accessing directories.
I used the
adLdap component because it is sharpened exactly under ActiveDirectory, it provides a simple and convenient OOP API and it’s just a pleasure to work with it.
First, I connected adLdap to Yii as an application component, i.e .:
The LdapComponent class itself:
Configuring adLdap is done by overriding its properties. I wanted to bring the settings of this component to the config in the usual format for the Yii programmer. Therefore, I redefined the required fields, changing their visibility attribute (so that Yii could configure the component) and transfer the constructor to the init () method so that the constructor is called AFTER The object will be configured (fields are filled).
Next, we can simply use this component like all the others in Yii:
Yii::app()->ldap
For authorization using LDAP, you need to create the standard components required for authorization in Yii:
UserIdentity and
WebUser :
In the code above, we override the methods authenticate of the CUserIdentity class in order to implement our authorization logic. We are trying to authorize this user in AD through adLdap, and if successful, then enter his name and email in the permanent storage.
In addition to LDAP, I decided to keep additional information about users in the database, so after successful authorization, it is checked whether there is already a row in the database for this user, and if not, it is created.
The LdapUser class is almost the same as the standard, except for the function LdapUser :: getGroups (), which is important for us. This function, as it is not difficult to guess, returns all groups from this user from AD.
I decided to make user groups in ActiveDirectory roles in the Yii application.
Those. We will assign roles not to specific users, but to groups. And which group to assign to will be managed centrally through AD.
This is very convenient in corporate portals and other internal resources, since together with the rights to printers, folders, and other office infrastructure, the user will immediately be given rights to sections in the corporate website. At the same time, the IT department specialists do not need to explain anything, they just do their job “as usual”.
In order to assign roles to users, I redefined the CPhpAuthManager class:
class PhpAuthManager extends CPhpAuthManager { public function init() {
In the code above, we take a list of groups in which the user is a member, check whether the role of the same name exists, and if it exists, then assign this role to the user.
An example of an authentication configuration file with LDAP looks like this:
... 'newsReader' => array( 'type' => CAuthItem::TYPE_ROLE, 'description' => '', 'bizRule' => NULL, 'data' => NULL, 'children' => array( 0 => 'readNews', ), ), 'newsAuthor' => array( 'type' => CAuthItem::TYPE_ROLE, 'description' => '', 'bizRule' => NULL, 'data' => NULL, 'children' => array( 'newsReader', 'createNews', 'updateOwnNews', 'deleteOwnNews' ), ), 'newsManager' => array( 'type' => CAuthItem::TYPE_ROLE, 'description' => '', 'bizRule' => NULL, 'data' => NULL, 'children' => array( 'newsReader', 'createNews', 'updateNews', 'deleteNews', ), ),
In the ROLES section, we describe “intermediate” roles. Then in the ROLES ASSIGMENTS section, we describe groups from AD, and assign intermediate roles to them.
The above config can be read like this:
All actions with news (newsManager) and with requests (requestManager) will be available for the
developers group, and only the creation of applications will be available for the
departamentBoss group.
Conclusion
The role mechanism in Yii is truly flexible if used correctly.
Future plans include the creation or adaptation of a GUI solution for managing roles, since even with a small number of actions, the system becomes confusing, and the amount of writings is unjustified.
I urge all users to discuss: how did they implement the rights management system in Yii projects, and what advice from personal experience might be useful to others?
What else to read: