I am using Access Control Filter for access managing, but can't get one thing done - for example, how can I allow just project manager to update project and forbid it to others? I tried it via matchCallback, but in this case all project managers can update any project because TRUE is returned.
Similar more often required rules - how to allow user to update/delete posts where he is author using ACF?
'access' => [
'class' => AccessControl::className(),
'only' => ['index', 'view', 'create', 'update', 'delete'],
'rules' => [
[
'actions' => ['update'],
'allow' => true,
'roles' => ['#'],
'matchCallback' => function ($rule, $action) {
return Yii::$app->user->identity->getProjectParticipants()
->one()->isManager(Yii::$app->user->identity->id);
}
],
],
],
It could be implemented something like this:
use Yii;
use yii\web\Controller;
use yii\filters\AccessControl;
class MyController extends Controller
{
...
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'only' => ['update', 'delete'],
'rules' => [
[
'actions' => ['update', 'delete'],
'allow' => true,
'roles' => ['#'],
'matchCallback' => function ($rule, $action) {
if (Yii::$app->user->can('admin') || $this->isUserAuthor()) {
return true;
}
return false;
}
],
],
],
];
}
protected function findModel($id)
{
if (($model = MyModel::findOne($id)) !== null) {
return $model;
} else {
throw new NotFoundHttpException('The requested page does not exist.');
}
}
protected function isUserAuthor()
{
return $this->findModel(Yii::$app->request->get('id'))->author->id == Yii::$app->user->id;
}
...
}
This is bested solved with a custom AccessRule. One would have to fill in the code to check if a user is the author of a project.
namespace app\filters;
class AuthorAccessRule extends \yii\filters\AccessRule
{
public $allow = true; // Allow access if this rule matches
public $roles = ['#']; // Ensure user is logged in.
public function allows($action, $user, $request)
{
$parentRes = parent::allows($action, $user, $request);
// $parentRes can be `null`, `false` or `true`.
// True means the parent rule matched and allows access.
if ($parentRes !== true) {
return $parentRes;
}
return ($this->getProjectAuthorId($request) == $user->id);
}
private function getProjectAuthorId($request)
{
// Fill in code to receive the right project.
// assuming the project id is given à la `project/update?id=1`
$projectId = $request->get('id');
$project = \app\models\Project::findOne($projectId);
return isset($project) ? $project->author_id : null;
}
}
The rule can be used by including this in the behaviors:
'authorAccess' => [
'class' => AccessControl::className(),
'only' => ['update'],
'rules' => ['actions' => ['update']],
'ruleConfig' => ['class' => '\app\filters\AuthorAccessRule'],
],
Following is how I do it with combination of ACF and RBAC. Please correct me if I am wrong or there is better way of doing it. It's based on Basic template.
User's role is stored in the "role" column of the "user" table. Another table "country" is used in this example. Assume you have generated models and controllers using Gii.
Customise PhpManager to use role from database table "user".
class PhpManager extends \yii\rbac\PhpManager
{
public function init()
{
parent::init();
}
public function getAssignments($userId)
{
if (!Yii::$app->user->isGuest) {
$assignment = new Assignment();
$assignment->userId = $userId;
# Assume the role is stored in User table "role" column
$assignment->roleName = Yii::$app->user->identity->role;
return [$assignment->roleName => $assignment];
}
}
}
3. Add authManager to web.app and console.app console file.
'authManager' => [
'class' => 'app\components\PhpManager',
'defaultRoles' => ['user', 'manager', 'admin', 'master'],
],
Create a customized AccessRule. Reference from speixoto's blog.
# Reference
# http://programming.peixoto.cf/2015/01/14/yii2-role-based-access-control-and-context-access-rule/#$$nmvkr0&&0SUmhOPVEeSW9grIhAgzZg$$
class ContextAccessRule extends AccessRule
{
public $modelClass;
public $primaryKey;
protected function matchRole($user)
{
if (parent::matchRole($user))
return true;
$model = $this->findModel();
foreach ($this->roles as $role) {
# Call the CheckAccess() function which process rules
if ($user->can($role, ['model' => $model])) {
return true;
}
}
return false;
}
protected function findModel()
{
if (!isset($this->modelClass))
throw new InvalidConfigException(Yii::t('app', 'the "modelClass" must be set for "{class}".', ['class' => __CLASS__]));
$primaryKey = $this->getPrimaryKey();
# Get the request params
$queryParams = \Yii::$app->getRequest()->getQueryParams();
# Load the model
$model = call_user_func([$this->modelClass, 'findOne'], $queryParams[join(',', $primaryKey)]);
if ($model !== null) {
return $model;
} else {
throw new NotFoundHttpException(Yii::t('app', 'The requested page does not exists.'));
}
}
# Get the primary key of the model
protected function getPrimaryKey()
{
if (!isset($this->primaryKey)) {
return call_user_func([$this->modelClass, 'primaryKey']);
} else {
return $this->primaryKey;
}
}
Create a RbacController.php to generate RBAC files (assignments.php, items.php, rules.php) into rbac folder.
class RbacController extends Controller
{
public function actionInit()
{
$auth = Yii::$app->authManager;
$auth->removeAll();
### CREATE & ADD ROLES
$user = $auth->createRole('user');
$node = $auth->createRole('node');
$manager = $auth->createRole('manager');
$admin = $auth->createRole('admin');
$master = $auth->createRole('master');
$auth->add($user);
$auth->add($node);
$auth->add($manager);
$auth->add($admin);
$auth->add($master);
$auth->addChild($manager, $user);
$auth->addChild($manager, $node);
$auth->addChild($admin, $manager);
$auth->addChild($master, $admin);
### ADD RULES
$ownerRule = new \app\components\OwnerRule();
$auth->add($ownerRule);
### CREATE PERMISSIONS ###
$pUpdateOwn = $auth->createPermission('updateOwn');
$pUpdateOwn->description = 'update own';
$pUpdateOwn->ruleName = $ownerRule->name;
$auth->add($pUpdateOwn);
$auth->addChild($pUpdateOwn, $pUpdate);
$pDeleteOwn = $auth->createPermission('deleteOwn');
$pDeleteOwn->description = 'delete own';
$pDeleteOwn->ruleName = $ownerRule->name;
$auth->add($pDeleteOwn);
$auth->addChild($pDeleteOwn, $pDelete);
### ASSIGN PERMISSION TO ROLES
$auth->addChild($user, $pUpdateOwn);
$auth->addChild($user, $pDeleteOwn);
$auth->addChild($manager, $pUpdateOwn);
$auth->addChild($manager, $pDeleteOwn);
}
}
From console, navigate to your project root. Run ./yii rbac/init (for mac) to generate the 3 files into rbac folder.
In CountryController.php, override following function to add "access" behaviors.
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['verbs'] = [
'class' => VerbFilter::className(),
'actions' => [
'delete' => ['post'],
],
];
['access'] = [
'class' => AccessControl::className(),
// 'only' => ['view', 'index', 'create', 'update', 'delete'],
'rules' => [
[
'actions' => ['view', 'index'],
'allow' => true,
'roles' => ['?', '#'],
],
[
'actions' => ['create'],
'allow' => true,
// Allow users, manager and admins to create
'roles' => ['user'],
],
[
'class' => 'app\components\ContextAccessRule',
'modelClass' => 'app\models\Country',
'actions' => ['update'],
'allow' => true,
# allow owner and manager to udpate
'roles' => ['updateOwn', 'manager']
],
[
'class' => 'app\components\ContextAccessRule',
'modelClass' => 'app\models\Country',
'actions' => ['delete'],
'allow' => true,
# allow owner and manager to delete
'roles' => ['deleteOwn', 'manager'],
],
],
# if user not login, and not allowed for current action, return following exception
'denyCallback' => function ($rule, $action) {
throw new UnauthorizedHttpException('You are not authorized.');
},
];
return $behaviors;
}
8. Test it out.
Related
I want to use two tables from different databases for login feature in Yii2 basic app.
In the login view, I add a new field:
<?= $form->field($model, 'username') ?>
<?= $form->field($model, 'password')->passwordInput() ?>
<?= $form->field($model, 'choice') ?>
In the LoginForm, I modify this:
public function getUser() {
if ($this->_user === false && $this->choice == 1) {
$this->_user = User::findByUsername($this->username);
}
else if ($this->_user === false && $this->choice == 2) {
$this->_user = UserPerusahaan::findByUsername($this->username);
}
return $this->_user;
}
The User.php has this:
public static function getDb() {
return \Yii::$app->dblogin; // use the "db2" application component
}
public static function tableName() {
return 'pengguna';
}
What makes UserPerusahaan.php different from User.php is this:
/*public static function getDb() {
return \Yii::$app->dblogin;
}*/
public static function tableName() {
return 'perusahaan';
}
When I try logging in it just refreshes the login page.
What am I missing here? Or is there another better practical approach?
EDIT:
I tried adding this to components in web.php:
'user' => [
'class' => 'yii\web\User',
'identityClass' => 'app\models\User',
'enableAutoLogin' => true,
],
'userperusahaan' => [
'class' => 'yii\web\User',
'identityClass' => 'app\models\UserPerusahaan',
'enableAutoLogin' => true,
],
And this to LoginForm.php:
public function login() {
if ($this->validate()&& $this->choice == 1) {
return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600 * 24 * 30 : 0);
}
else if ($this->validate()&& $this->choice == 2) {
return Yii::$app->userperusahaan->login($this->getUser(), $this->rememberMe ? 3600 * 24 * 30 : 0);
}
return false;
}
The login using $choice = 1 works, but using $choice = 2 still gives me refreshed login page.
If you use that scenario, avoid using roles "#" in your accessControl. Roles "#" only applied to Yii::$app->user, so if you login with different component (such as Yii::$app->userPerusahaan->login()) it will not count as registered user with roles "#". Modified your siteController like this example.
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'rules' => [
[
'actions' => ['index', 'login'],
'allow' => true,
'roles' => ['?'],
],
[
'actions' => ['logout'],
'allow' => true,
'roles' => ['#'],
],
],
],
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'logout' => ['post'],
],
],
];
}
public function actionIndex()
{
if(Yii::$app->user->isGuest && Yii::$app->userPerusahaan->isGuest) return $this->redirect(['login']);
// ......
In my source code, I have this code for check if a user have permissions. For this I'm using RBAC. In my controller, I have this:
public function behaviors()
{
$behaviors['access'] = [
'class' => AccessControl::className(),
'rules' => [
[
'allow' => true,
'roles' => ['#'],
'matchCallback' => function ($rule, $action) {
$module = Yii::$app->controller->module->id;
$action = Yii::$app->controller->action->id;
$controller = Yii::$app->controller->id;
$route = "$module/$controller/$action";
$post = Yii::$app->request->post();
if (\Yii::$app->user->can($route)) {
return true;
} else {
Yii::$app->session->setFlash('error', 'Your user does not have access to this module.');
return $this->redirect('dashboard');
}
}
],
],
];
return $behaviors;
}
My issue is when I use the function
if (\Yii::$app->user->can($route)) { ....
Yii displays the following error:
PHP Warning – yii\base\ErrorException
in_array() expects parameter 2 to be array, string given ....
........
in /lxcshared/yii-develop/sacyii.git/vendor/yiisoft/yii2/rbac/DbManager.php at line 196
........
if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
return true;
}
I follow this guideline Role Based Access Control (RBAC) but I don't know what why I have this issue.
Check your config authManager->defaultRoles. It must be array.
'authManager' => [
...
'defaultRoles' => [...]
]
I am working on Yii2 rest API, When I call create action of enquiryontroller then I am getting this error : "NetworkError: 405 Method Not Allowed".
And also I go through YII2 documentation but not able to trace my issue.
Please check and revert, it will be a great help.
Here is controller code that is EnquiryController.php :
<?php
namespace frontend\controllers;
use Yii;
use common\models\Enquiry;
use yii\filters\ContentNegotiator;
use yii\web\Response;
use yii\filters\AccessControl;
use yii\rest\ActiveController;
use yii\filters\auth\HttpBearerAuth;
use yii\filters\VerbFilter;
use yii\data\ActiveDataProvider;
class EnquiryController extends ActiveController
{
/**
* #inheritdoc
*/
public $modelClass = 'common\models\Enquiry';
public $serializer = [
'class' => 'yii\rest\Serializer',
'collectionEnvelope' => 'items',
];
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => HttpBearerAuth::className(),
];
$behaviors['contentNegotiator'] = [
'class' => ContentNegotiator::className(),
'formats' => [
'application/json' => Response::FORMAT_JSON,
],
];
return $behaviors;
}
public function actions()
{
$actions = parent::actions();
// disable the "delete" and "create" actions
unset($actions['create']);
unset($actions['delete'], $actions['view']);
unset($actions['index']);
// customize the data provider preparation with the "prepareDataProvider()" method
return $actions;
}
public function actionCreate()
{
$model = new Enquiry();
return Yii::$app->getRequest()->getBodyParams();
if ($model->load(Yii::$app->getRequest()->getBodyParams(), '') && $model->validate()) {
$model->slug = \common\components\Helper::slugify($model->title);
$model->user_id = Yii::$app->user->id;
$model->save();
//mail functionality
return true;
}
return $model;
}
}
and code in config/main-local.php :
'urlManager' => [
'class' => 'yii\web\UrlManager',
'baseUrl' => $baseUrl,
'enablePrettyUrl' => true,
'showScriptName' => false,
//'enableStrictParsing' => true,
'rules' => [
['class' => 'yii\rest\UrlRule', 'controller' =>['api'], 'pluralize'=>true],
],
],
],
'as access' => [
'class' => 'mdm\admin\components\AccessControl',
'allowActions' => [
'site/*',
'api/login',
'profile/*',
'api/activate-user',
'api/contact',
'home/*',
'post/*',
'pages/*',
'categories/*',
'guestbook/*',
'faq/*',
'news/*',
'events/*',
'enquiry/*',
'partners/*',
'api/signup'// add or remove allowed actions to this list
]
],
Have a look to this guide
// disable the "delete" and "create" actions
unset($actions['delete'], $actions['create']);
because in your code you disable the create, delete, view and index action
public function actions()
{
$actions = parent::actions();
// disable the "delete" and "create" actions ?????
unset($actions['create']); ////????
unset($actions['delete'], $actions['view']); /// ????
unset($actions['index']); ////????
// customize the data provider preparation with the "prepareDataProvider()" method
return $actions;
}
I've this code for the controller (I'm using the basic template for testing purposes:
use Yii;
use yii\filters\AccessControl;
use yii\web\Controller;
use yii\filters\VerbFilter;
use app\models\LoginForm;
use app\models\ContactForm;
class SiteController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'only' => ['logout'],
'rules' => [
[
'actions' => ['logout'],
'allow' => true,
'roles' => ['#'],
],
],
],
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'logout' => ['post'],
],
],
];
}
public function actions()
{
return [
'error' => [
'class' => 'yii\web\ErrorAction',
],
'captcha' => [
'class' => 'yii\captcha\CaptchaAction',
'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
],
];
}
public function actionIndex()
{
$param = $this->somefunction();
return $this->render('index', [
"param" => $param
]);
}
public function actionLogin()
{
if (!\Yii::$app->user->isGuest) {
return $this->goHome();
}
$model = new LoginForm();
if ($model->load(Yii::$app->request->post()) && $model->login()) {
return $this->goBack();
} else {
return $this->render('login', [
'model' => $model,
]);
}
}
public function actionLogout()
{
Yii::$app->user->logout();
return $this->goHome();
}
public function actionContact()
{
$param = $this->somefunction();
$model = new ContactForm();
if ($model->load(Yii::$app->request->post()) && $model->contact(Yii::$app->params['adminEmail'])) {
Yii::$app->session->setFlash('contactFormSubmitted');
return $this->refresh();
} else {
return $this->render('contact', [
'model' => $model,
"param" => $param
]);
}
}
public function actionAbout()
{
$param = $this->somefunction();
return $this->render('about', [
"param" => $param
]);
}
}
As you can notice, I'm passing param to multiple views with the same content so I want some time-saving way to send it only one time to all views.
Is that possible?
Currently, I'm using the session to store values and call the session in required views. I want something more convenience
You can use Controller and View events to achieve that.
Add this to your controller:
use yii\web\View;
...
public function beforeAction($action)
{
if (!parent::beforeAction($action)) {
return false;
}
$this->view->on(View::EVENT_BEFORE_RENDER, function() {
$this->view->params['param'] = ...;
});
return true;
}
If you want it in multiple controllers, either create another controller (extending from yii\web\Controller) for that and override beforeAction and then extend your controllers from custom one, or set it during application boostrap using BoostrapInterface.
Additionally you can add some condition:
if (in_array($action, ['create', 'update']) {
...
}
and custom param will be only passed in these actions.
in your controller you can redefine the init () function thanks to which you can assign to a variable of the controller the value you're interested
I have a rest full API in Yii2. I have the user model, controller and created a new rest action called login.
How can I set the action login to be executed by the guest users?
class UserController extends \yii\rest\ActiveController{
public function actions() {
$actions = parent::actions();
$actions['login'] = [
'class' => 'app\modules\user\actions\user\LoginUserAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
];
return $actions;
}
public function checkAccess($action, $model = null, $params = array()) {
return true;
}
}
You can set the access control rules on the behaviors function of your controller, instead of on the checkAccess property on the action:
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['access'] = [
'class' => AccessControl::className(),
'except' => ['login'],
'rules' => [
[
'allow' => true,
'actions' => ['foo', 'foo2'],
'roles' => ['admin'],
]
],
];
return $behaviors;
}
In this example, the access control is applied to all actions except 'login'. I left the rules part so you can have an example of how you make customized rules.