You are viewing revision #8 of this wiki article.
This is the latest version of this article.
You may want to see the changes made in this revision.
One of the common requests I see in the forum is how to implement RBAC. While you can implement Yii 2's built-in RBAC, that might be too much for developers who are just starting with Yii 2 or have simpler needs. Sometimes you are looking for a fast solution and just want two flavors, user and admin. And even if you will eventually need more, you can use these methods as a starting point for developing your own features or move on to Yii 2's RBAC.
So this is a variation on my own implementation which is more involved, but this will get you the basics quickly. Using Yii 2's advanced template, 99.9 % of the work is done before you start. So here is what we'll do:
- add constants to the User model for admin and user role & add a role column on user table, type int, not null, default value 10.
- add the contstants into the range of values for user roles
- create a static method on the User model to check isUserAdmin
- create a loginAdmin method on the LoginForm model
- change the backend site controller to use loginAdmin method on login
- add an access control rule in the behaviors method to the frontend site controller to restrict access to the about page to logged in admin only
The last one I really love. We use Yii 2's behaviors to enforce our RBAC, the method for this is already built and intuitive. You will love how easy all this really is.
These instructions are for a fresh install of the advanced template, so it's assumed you have no existing code that will conflict. You can get the advanced template installation instructions here.
Ok, Step 1:
Add this to the top of your User model in common\models\User
const ROLE_USER = 10;
const ROLE_ADMIN = 20;
We use the constant to set the value of admin to 20. We are going to use the Role value on the user record to compare to this number. So, if the user's role is set to 20, they are admin. The default on signup is 10, so the only way for a user to get 20 on role at this point is for you to assign it directly in the DB (probably through PhpMyAdmin).
Since I wrote this tutorial orginally, Yii 2 dropped the role column from the advanced application in it's out-of-the-box build, so you will have to add a role column to your user table manually before continuing on. Make the role column int, not null, and default value = 10.
Step 2
Under the Rules method in the User model, add the following:
['role', 'default', 'value' => 10],
['role', 'in', 'range' => [self::ROLE_USER, self::ROLE_ADMIN]],
We add default rule and simply limit the allowed range of values for role to the two that we have defined in our constants.
Step 3
Add the following method at the bottom of the User model:
public static function isUserAdmin($username)
{
if (static::findOne(['username' => $username, 'role' => self::ROLE_ADMIN])){
return true;
} else {
return false;
}
}
Ok, so we gave this method a very intuitive name. It looks up the user by the findOne method, where the $username is what we hand into the method and where the role is equal to the value we set on ROLE_ADMIN constant. If user is admin, we return true, if not, false.
We made it a static method, so we can keep the call to the method very concise and limit the clutter into other methods where we will use it.
Step 4
In common\models\LoginForm
Add the following method:
public function loginAdmin()
{
if ($this->validate() && User::isUserAdmin($this->username)) {
return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600 * 24 * 30 : 0);
} else {
return false;
}
}
Simple, we are adding an additional condition to the if statement. Now, not only does it have to validate, but now User::isUserAdmin($this->username) has to evaluate to true.
Step 5
in backend\controllers\SiteController.php, change actionLogin to:
public function actionLogin()
{
if (!\Yii::$app->user->isGuest) {
return $this->goHome();
}
$model = new LoginForm();
if ($model->load(Yii::$app->request->post()) && $model->loginAdmin()) {
return $this->goBack();
} else {
return $this->render('login', [
'model' => $model,
]);
}
}
There is only one slight change between this code and the out-of-the-box, we are just using loginAdmin instead of login method.
Once you've made this change, any user trying to login to the backend will have to have a role value equal to that set by the ROLE_ADMIN constant on the User model. So now we just need to be able to enforce our access to the actions. There is a super simple way to do this using behaviors and the matchCallback method under rules.
I'm going to use the frontend about page as an example because the frontend site controller is already using access rules in it's behaviors method, so it's really easy to demonstrate how to use this.
Step 6
In frontend\controllers\SiteController.php, include the use statement at the top of the file:
use common\models\User;
Then modify the behaviors method in the same file, frontend\controllers\SiteController.php to:
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'only' => ['logout', 'signup', 'about'],
'rules' => [
[
'actions' => ['signup'],
'allow' => true,
'roles' => ['?'],
],
[
'actions' => ['logout'],
'allow' => true,
'roles' => ['@'],
],
[
'actions' => ['about'],
'allow' => true,
'roles' => ['@'],
'matchCallback' => function ($rule, $action) {
return User::isUserAdmin(Yii::$app->user->identity->username);
}
],
],
],
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'logout' => ['post'],
],
],
];
}
So what we did that's different from what was already there. We modified: ~~~ 'only' => ['logout', 'signup', 'about'], ~~~
So now it applies to the about action as well. Then we added a block of rules in an array:
[
'actions' => ['about'],
'allow' => true,
'roles' => ['@'],
'matchCallback' => function ($rule, $action) {
return User::isUserAdmin(Yii::$app->user->identity->username);
}
],
So this rule only applies to the about action. We use the matchCallback method, which needs to return true or false, to see if the current user, whom we have supplied by feeding in Yii::$app->user->identity->username, has a role of admin. If it returns true, the action is allowed, if not, it says You are not allowed to perform this action.
Obviously note that we also required logged in state for the about action because without the user being logged in, we can't do the call to determine the user's role.
This will work on any controller as long as you are implementing the behaviors method as described above. This means you can use this simple RBAC to control every action on the site, frontend or backend, if you wish.
So there you have it, super simple RBAC to get you started. Yii 2 does almost all the work before you even start. I've looked at many PHP frameworks and I've never seen one help you this much. We got so much power with so little code. If you want UI to set user's roles, just use Gii to create the user crud in the backend. Just remember to get the namespaces right, the core User model is in common\models.
Obviously, this is just a starter approach. If you just need two roles, this is fine. If you are doing an enterprise level application, you will probably need more, but you could still start with this to learn your way around Yii 2. I like to use the advance template because they give you a working user model out-of-the-box and you can see how quickly we were able to move along with that. I really love this framework.
Correction
When Login
if ($this->validate() && User::isUserAdmin($this->username)) { ...
If user user is not admin, what it happen?
isUserAdmin method
Why it is declarated as static. more simple if its not static.
public function getIsUserAdmin() { return $this->role == static::ROLE_ADMIN; } // 'matchCallback' => function ($rule, $action) { return Yii::$app->user->identity->isUserAdmin; }
response to comment
Hi, thanks for your input. To answer your first question, login fails if the user is not admin.
I tested your suggested code on the loginAdmin method and it returned an error, so I did not bother to try it on matchCallback. When I wrote the tutorial, I intended to use one method in multiple places to keep it as simple as possible to implement, and the way I did it works.
If you have a suggestion for optimization or a better way to do it, that is indeed welcome, but please test and verify code before posting, thanks.
.
Its not about optimation or something. I just want to try understanding your article.
First, "login fails if the user is not admin". with what
not admin
sign in? are you suggest more than one login page? So, my suggestion, you dont need to check user is admin or not admin when login.Second. I assume that user login success. Everithing you need to check authorization for admin, you can use
Yii::$app->user->identity->isUserAdmin
.PS: I guess the error appear when calling code
User::isUserAdmin($this->username)
. As i say, we dont need this..
.
.
.
.
.
.
.
.
.
$user = $this->getUser(); if ($this->validate() && $user->isUserAdmin) { return Yii::$app->user->login($user, $this->rememberMe ? 3600 * 24 * 30 : 0); }
This tutorial is for advanced template
Yes, to answer your question about login, there is more than one site controller and more than one site index page on the Yii 2 advanced template, which is what this tutorial uses. Those instructions about using the advanced template are right before step one in the tutorial. So your suggestion for not needing to check the role of user on login does not work with this tutorial or the direction of this article at all. I appreciate the fact that you are trying to help, but if you are going to do that, you really need implement the example correctly before trying to change it. That way you can understand it before trying to modify or improve it.
Some suggestion & improvement
For record, i am not a pro programmer but have fair enough working skills. Now thats clear; I have 1 problem with your solution.
If an user try to login at backend with VALID Credentials; The system will redirect user to login page bcoz the role was not admin, Without any warning/error message of what have happened. Though the user entered valid credential but invalid role. Still in my opinion there should be a message of invalid login. Invalid bcoz admin role is only valid login with correct credentials. For that what i have done is as follows in code:
public function loginAdmin() { if ("validate user and verify admin role") return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600 * 24 * 30 : 0); else { $this->addError('password', 'Incorrect username or password.'); return false; } }
I didn't knew how to set a flashmessage for the error like the original default login page shows, so i did a print_r($this) and found that the errors the login outputs is saved with "password" key on invalid credential, so i used that and made the invalid credential error.
Secondly:
Your solution uses an extra query in isUserAdmin() which could be saved by the data we already have.
I think the user whom your were talking to in comments "Misbahul D Munir" his solution is fair enough. The following is a working model of your and Misbahul's logic combined.
Backend Controller:
Rules: 'only' => ['index', 'login', 'logout'], 'rules' => [ [ 'actions' => ['login'], 'allow' => true, ], [ 'actions' => ['logout', 'index'], 'allow' => true, 'roles' => ['@'], 'matchCallback' => function ($rule, $action) { return Yii::$app->user->identity->isAdmin; } ], Action: public function actionLogin() { if (!\Yii::$app->user->isGuest) { return $this->goHome(); } $model = new LoginForm(); if ($model->load(Yii::$app->request->post()) && $model->loginAdmin()) { return $this->goBack(); } else { return $this->render('login', [ 'model' => $model, ]); } }
Frontend Controller:
Rules: 'only' => ['logout', 'signup'], 'rules' => [ [ 'actions' => ['signup'], 'allow' => true, 'roles' => ['?'], ], [ 'actions' => ['logout'], 'allow' => true, 'roles' => ['@'], 'matchCallback' => function ($rule, $action) { return Yii::$app->user->identity->isUser; } ], Action: public function actionLogin() { if (!\Yii::$app->user->isGuest) { return $this->goHome(); } $model = new LoginForm(); if ($model->load(Yii::$app->request->post()) && $model->loginUser()) { return $this->goBack(); } else { return $this->render('login', [ 'model' => $model, ]); } }
Login Form:
public function loginUser() { if ($this->validate() && $this->_user->isUser) return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600 * 24 * 30 : 0); else { $this->addError('password', 'Incorrect username or password.'); return false; } } public function loginAdmin() { if ($this->validate() && $this->_user->isAdmin) return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600 * 24 * 30 : 0); else { $this->addError('password', 'Incorrect username or password.'); return false; } }
I have used ($this->_user->isAdmin)
We can use the following but its unnecessary bcoz the validate() function already call the $this->getUser() when validates password is called. which is defines in rules section whn to be and for which entity to be invoked on.
$user = $this->getUser(); if ($this->validate() && $user->isAdmin)
Correct or guide me if i'm wrong or not doing thing the right way. I'm just learning :)
Feel free to improve it.
The reason I did not supply a failed admin login message that exposes a failed role match is that I don't think it's a good idea make that information public, especially when it comes to admin login. Feel free to do as you wish on that.
I have not successfully tested your code based on what you have supplied, so I can't verify that it works. You have obviously changed things and added methods isUser, isAdmin, etc.
On the surface, it doesn't look like it would get past $this->_user without running getUser first, since that value will always be false, unless the getUser method is run. I may be missing something, but the validate method originates on Model, and that does not call getUser. This is why in the return statement, Yii 2 is not relying on _user, but instead calling getUser. They would not do that if they aleady had the value by other syntax, would they? Anyway, you are saying you have a working model, so I will take your word on it.
I don't have time to fully debug it and that is not really necessary, since if you have a working solution you are happy with, then you should use it.
I wrote Super Simple RBAC to use a single, re-usable method to check role in both the controller and LoginForm model, so I could make the instructions as clear and as easy to follow as possible. It's meant to be a starting point that gets you up and running quickly.
I'm happy for anyone who can improve it and make the code more efficient. Have fun with it.
"RBAC with Admin and User"
I got excited at first when I saw your article tile. I thought you will simplify the implementation of the yii2-admin and yii2-user modules for the advanced template. I'm having a horrible time trying to get these two working properly (with PhpManager as authManager).
@Misbahul, do you have anything like that documented?
Thanks
Question regarding template
Firstly, thanks for this.. very helpful!
Can this be implemented with the basic template? I don't need the advanced template but did want to implement a basic RBAC.
addition to loginAdmin
I throw a model error if user fail LoginAdmin check. They dont need to know that this "backend" is for admin only. I want to show that there are some "error" so they know something is wrong
public function loginAdmin() { if ($this->validate() && User::isUserAdmin($this->username)) { return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600 * 24 * 30 : 0); } else { $this->addError('password', 'Incorrect username or password.'); return false; } }
Is isUserAdmin function expecting username or id?
In the part of frontend\controllers\SiteController where you check for admin status with:
[code]
return User::isUserAdmin(Yii::$app->user->identity->username);
[/code]
For some reason I could not get it to work unless I changed it to:
[code]
return User::isUserAdmin(Yii::$app->user->identity->id);
[/code]
I have a doubt
As would be the case in the function (isUserAdmin) if the roles are in another table (rbac_auth_assignment)??????
Code in Step 3 can be optimized using AR exists() method:
public static function isUserAdmin($username) { return self::find(['username' => $username, 'role' => self::ROLE_ADMIN])->exists(); }
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.