yiisoft / yii2

Yii 2: The Fast, Secure and Professional PHP Framework
http://www.yiiframework.com
BSD 3-Clause "New" or "Revised" License
14.24k stars 6.91k forks source link

RBAC not working properly in Yii2 #18080

Closed IntegrityFoundation closed 4 years ago

IntegrityFoundation commented 4 years ago

Hi, I am using Yii2 basic. I two tables admin and employee. Both tables have id which is primary key, username, password. I have implemented the scenario where user can login from two different tables. Now I have RBAC implementation. Here I have created four tables which are required for RBAC. Now I have also created rules, permissions and roles in RBAC init(). I have assigned permissions to roles and roles to users.

I have a MultiUser.php model where it checks the instance of user whether it is of admin or employee and on that basis the user logs in.

Following is the MultiUser.php model

<?php

namespace app\models;

use app\models\Employee;
use app\models\Admin;

class MultiUser extends \yii\base\BaseObject implements \yii\web\IdentityInterface
{
    /**
     * @var \yii\db\BaseActiveRecord
     */
    private $_user;

    /**
     * {@inheritdoc}
     */
    public static function findIdentity($id)
    {
        // $id will be something like "admin-1" or "employee-1"
        list($type, $pk) = explode('-', $id);

        switch ($type) {
            case 'admin':
                $model = Admin::findOne($pk);
                break;
            case 'employee':
                $model = Employee::findOne($pk);
                break;
        }

        if (isset($model)) {
            return new static(['user' => $model]);
        }
    }

    /**
     * {@inheritdoc}
     */
    public static function findIdentityByAccessToken($token, $type = null)
    {
        throw new \yii\base\NotSupportedException();
    }

    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        $prefix = $this->_user instanceof Admin ? 'admin' : 'employee';
        return $prefix . '-' . $this->_user->getPrimaryKey();
    }
    public function getOnlyid()
    {
        //$prefix = $this->_user instanceof Admin ? 'admin' : 'employee';

        return  $this->_user->getPrimaryKey();
    }

    /**
     * {@inheritdoc}
     */
    public function getAuthKey()
    {
        $secret = \Yii::$app->request->cookieValidationKey;

        return sha1($secret . get_class($this->_user) . $this->_user->getPrimaryKey());
    }

     public function validateAuthKey($authKey)
    {
        $secret = Yii::$app->request->cookieValidationKey;
        $computedKey = sha1($secret . get_class($this->_user) . $this->_user->getPrimaryKey());

        return $authKey === $computedKey;
    }

    public function setUser(\yii\db\BaseActiveRecord $user)
    {
        $this->_user = $user;
    }

    public function getUsername()
    {
        return $this->_user->username;
    }
}

I have a table called groupdetails which has GroupId. employee table has a relation with grouodetails table. meaning One employee has many groupdetails. I created the CRUD for groupdetails. I have also checked for authorization in controller

Following is the Groupdetails controller.

<?php

namespace app\controllers;

use Yii;
use app\models\Groupdetails;
use app\models\GroupdetailsSearch;
use yii\web\UploadedFile;
use yii\web\Controller;

use yii\filters\VerbFilter;
use app\models\FieldofficerRule;
use yii\filters\AccessControl;
use yii\web\NotFoundHttpException;
use yii\web\ForbiddenHttpException;
/**
 * GroupdetailsController implements the CRUD actions for Groupdetails model.
 */
class GroupdetailsController extends Controller
{
    /**
     * {@inheritdoc}
     */
    public function behaviors()
    {
        return [
            'verbs' => [
                'class' => VerbFilter::className(),
                'actions' => [
                    'delete' => ['POST'],
                ],
            ],
        ];
    }

    /**
     * Lists all Groupdetails models.
     * @return mixed
     */
    public function actionIndex()
    {

       $searchModel = new GroupdetailsSearch();

        $dataProvider = $searchModel->search(Yii::$app->request->queryParams);

        return $this->render('index', [
            'searchModel' => $searchModel,
            'dataProvider' => $dataProvider,
        ]);
    }

    /**
     * Displays a single Groupdetails model.
     * @param integer $id
     * @return mixed
     * @throws NotFoundHttpException if the model cannot be found
     */
    public function actionView($id)
    {
        $model=$this->findModel($id);

        $Id=\Yii::$app->user->id;

 if(\Yii::$app->user->can('viewGroup',['model'=>$model]))
        {
        return $this->render('view', [
            'model' => $this->findModel($id),
        ]);
        }
        else
        {
           throw new ForbiddenHttpException("You can't view other groups details");
        }
    }

    /**
     * Creates a new Groupdetails model.
     * If creation is successful, the browser will be redirected to the 'view' page.
     * @return mixed
     */
    public function actionCreate()
    {

         if(\Yii::$app->user->can('createGroup'))
        {
        //$this->layout = 'layoutfile';
        $model = new Groupdetails();

       if ($model->load(Yii::$app->request->post()) ) {

            $model->id=\Yii::$app->user->identity->getOnlyid();
            $model->save(false);

            return $this->redirect(['view', 'id' => $model->GroupId]);
        } else {
            return $this->render('create', [
                'model' => $model,

            ]);
        }
        }
        else
        {

           throw new ForbiddenHttpException("You have no access to create groups");
        }

    }

    /**
     * Updates an existing Groupdetails model.
     * If update is successful, the browser will be redirected to the 'view' page.
     * @param integer $id
     * @return mixed
     * @throws NotFoundHttpException if the model cannot be found
     */
    public function actionUpdate($id)
    {
        $model = $this->findModel($id);

        if(\Yii::$app->user->can('updateGroup', ['model' => $model]))
        {

        if ($model->load(Yii::$app->request->post())) {

                $model->id=\Yii::$app->user->identity->getOnlyid();
                    $model->save();

            return $this->redirect(['view', 'id' => $model->GroupId]);
        }
        else {
            return $this->render('update', [
                'model' => $model,

            ]);
        }
        }

        else
        {
             throw new ForbiddenHttpException("You can't update other groups details");
        }

    }

    /**
     * Deletes an existing Groupdetails model.
     * If deletion is successful, the browser will be redirected to the 'index' page.
     * @param integer $id
     * @return mixed
     * @throws NotFoundHttpException if the model cannot be found
     */
    public function actionDelete($id)
    {
        $model = $this->findModel($id);
        if(\Yii::$app->user->can('deleteGroup', ['model' => $model]))
        {

        $model->delete();

        return $this->redirect(['index']);
        }
        else
        {
          throw new ForbiddenHttpException("You can't delete other groups details");
        }

    }

    /**
     * Finds the Groupdetails model based on its primary key value.
     * If the model is not found, a 404 HTTP exception will be thrown.
     * @param integer $id
     * @return Groupdetails the loaded model
     * @throws NotFoundHttpException if the model cannot be found
     */
    protected function findModel($id)
    {
        if (($model = Groupdetails::findOne($id)) !== null) {
            return $model;
        }

        throw new NotFoundHttpException('The requested page does not exist.');
    }
}

In auth_assignment table I have the following,

image1

Now when the user logs in from admin table and acccess groupdetails he can have access to create, view, update and delete actions.

But now when the user logs in from employee table he can create a record for groupdetails but he cant view, update, delete his groupdetails.

Below is my rule for RBAC

<?php

namespace app\rbac;

use yii\rbac\Rule;
use app\models\Groupdetails;

/**
 * Checks if authorID matches user passed via params
 */
class FieldofficerRule extends Rule
{
    public $name = 'isfieldofficer';

    /**
     * @param string|int $user the user ID.
     * @param Item $item the role or permission that this rule is associated with
     * @param array $params parameters passed to ManagerInterface::checkAccess().
     * @return bool a value indicating whether the rule permits the role or permission it is associated with.
     */
    public function execute($user, $item, $params)
    {
        return isset($params['model']) ? $params['model']->id == $user : false;
    }
}

No I have only two roles i.e admin and Field Officer. So I have following in RBAC controller.

<?php
namespace app\commands;

use Yii;
use yii\console\Controller;

class RbacController extends Controller
{
    public function actionInit()
    {
        $auth = Yii::$app->authManager;
     // $auth->removeAll(); //remove previous rbac.php files under console/data

        $rule = new \app\rbac\FieldofficerRule;
        $auth->add($rule);

        $indexAll = $auth->createPermission('indexAll');
        $indexAll->description = 'Index Page';
        $auth->add($indexAll);

        $createGroup = $auth->createPermission('createGroup');
        $createGroup->description = 'Create a Group';
        $auth->add($createGroup);

        $updateGroup = $auth->createPermission('updateGroup');
        $updateGroup->description = 'Update Group';
        $auth->add($updateGroup);

        $viewGroup = $auth->createPermission('viewGroup');
        $viewGroup->description = 'View Group';
        $auth->add($viewGroup);

        $deleteGroup = $auth->createPermission('deleteGroup');
        $deleteGroup->description = 'Delete Group';
        $auth->add($deleteGroup);

        $fieldofficer = $auth->createRole('Field Officer');
        $auth->add($fieldofficer);
        $auth->addChild($fieldofficer, $createGroup);

        $admin = $auth->createRole('Admin');
        $auth->add($admin);

        $auth->addChild($admin, $fieldofficer);
        $auth->addChild($admin, $updateGroup);
        $auth->addChild($admin, $viewGroup);
        $auth->addChild($admin, $deleteGroup);
        $auth->addChild($admin, $indexAll);

        $updateOwnGroup = $auth->createPermission('updateOwnGroup');
        $updateOwnGroup->description = 'Update own Group';
        $updateOwnGroup->ruleName = $rule->name;
        $auth->add($updateOwnGroup);

        $viewOwnGroup = $auth->createPermission('viewOwnGroup');
        $viewOwnGroup->description = 'View own Group';
        $viewOwnGroup->ruleName = $rule->name;
        $auth->add($viewOwnGroup);

        $deleteOwnGroup = $auth->createPermission('deleteOwnGroup');
        $deleteOwnGroup->description = 'Delete own Group';
        $deleteOwnGroup->ruleName = $rule->name;
        $auth->add($deleteOwnGroup);

        $auth->addChild($viewOwnGroup, $viewGroup);
        $auth->addChild($updateOwnGroup, $updateGroup);
        $auth->addChild($deleteOwnGroup, $deleteGroup);

        $auth->addChild($fieldofficer, $updateOwnGroup);
        $auth->addChild($fieldofficer, $viewOwnGroup);

        $auth->addChild($fieldofficer, $deleteOwnGroup);

    }
}
?>

When I place Yii::debug($params) in rule as follows

public function execute($user, $item, $params)
    {
        Yii::debug($params);
        return isset($params['model']) ? $params['model']->id == $user : false;

    }

Then in debug log, I have below

[
    'model' => unserialize('O:23:"app\\models\\Groupdetails":10:{s:36:"' . "\0" . 'yii\\db\\BaseActiveRecord' . "\0" . '_attributes";a:20:{s:7:"GroupId";i:26;s:21:"IdentificationDetails";i:4;s:9:"GroupName";s:11:"Raju Bachat";s:12:"TotalMembers";s:1:"3";s:12:"StageOfGroup";i:3;s:13:"ResidenceType";s:5:"Rural";s:13:"FormationType";s:4:"Open";s:13:"FormationDate";s:10:"2020-06-01";s:8:"Category";s:4:"Open";s:3:"IGA";s:3:"Yes";s:16:"RegistrationWith";s:8:"IF/Other";s:12:"RegisterUsed";s:8:"IF/Other";s:12:"PassBookUsed";s:8:"IF/Other";s:11:"MeetingDate";s:10:"2020-06-11";s:13:"MonthlySaving";d:1000;s:13:"BankAccountNo";s:11:"10212512022";s:15:"AccountOpenDate";s:10:"2020-06-01";s:8:"BankName";s:20:"Punjab National Bank";s:6:"Branch";s:10:"Ahmednagar";s:5:"Photo";s:4:"NULL";}s:39:"' . "\0" . 'yii\\db\\BaseActiveRecord' . "\0" . '_oldAttributes";a:20:{s:7:"GroupId";i:26;s:21:"IdentificationDetails";i:4;s:9:"GroupName";s:11:"Raju Bachat";s:12:"TotalMembers";s:1:"3";s:12:"StageOfGroup";i:3;s:13:"ResidenceType";s:5:"Rural";s:13:"FormationType";s:4:"Open";s:13:"FormationDate";s:10:"2020-06-01";s:8:"Category";s:4:"Open";s:3:"IGA";s:3:"Yes";s:16:"RegistrationWith";s:8:"IF/Other";s:12:"RegisterUsed";s:8:"IF/Other";s:12:"PassBookUsed";s:8:"IF/Other";s:11:"MeetingDate";s:10:"2020-06-11";s:13:"MonthlySaving";d:1000;s:13:"BankAccountNo";s:11:"10212512022";s:15:"AccountOpenDate";s:10:"2020-06-01";s:8:"BankName";s:20:"Punjab National Bank";s:6:"Branch";s:10:"Ahmednagar";s:5:"Photo";s:4:"NULL";}s:33:"' . "\0" . 'yii\\db\\BaseActiveRecord' . "\0" . '_related";a:0:{}s:47:"' . "\0" . 'yii\\db\\BaseActiveRecord' . "\0" . '_relationsDependencies";a:0:{}s:23:"' . "\0" . 'yii\\base\\Model' . "\0" . '_errors";N;s:27:"' . "\0" . 'yii\\base\\Model' . "\0" . '_validators";N;s:25:"' . "\0" . 'yii\\base\\Model' . "\0" . '_scenario";s:7:"default";s:27:"' . "\0" . 'yii\\base\\Component' . "\0" . '_events";a:0:{}s:35:"' . "\0" . 'yii\\base\\Component' . "\0" . '_eventWildcards";a:0:{}s:30:"' . "\0" . 'yii\\base\\Component' . "\0" . '_behaviors";a:0:{}}'),
]
alex-code commented 4 years ago

Can you clarify some details.

You've got an Admin table and a User table with id as the PK Your id PK of the GroupDetails table is the same as the user id.

Do all users get an admin and employee entry otherwise you've got a conflict where different users will have the same id.

e.g. Admin ID User
1 Joe Bloggs
Employee ID User
1 John Smith

They'll both share the same row in the GroupDetails table.

IntegrityFoundation commented 4 years ago

Hi,

Admin Table

id // primary key of admin table. Name username password

Employee Table

id // Primary key of Employee table Name username password

Groupdetails Table

GroupId // This is the Primary key of Groupdetails id // This is the foreign key. One employee can have many groupdetails. GroupName Stage

The users in admin table will have access to CRUD in groupdetails whereas the users in employee table will only create, update, view and delete their own groupdetails.

Each user will log in either from admin or employee table. This is working fine. But users from employee table who have created their own groupdetails cant view, update and delete it.

alex-code commented 4 years ago

In your rule, what does $user contain? You only save the integer part of the id in GroupDetails but it may have your prefix.

IntegrityFoundation commented 4 years ago

Yes. For example if the user is employee-2 then in groupdetails table id will be 2.

alex-code commented 4 years ago

So what does $user contain? If it's your prefixed id it's never going to match.

IntegrityFoundation commented 4 years ago

If we checked from log, we will find the the identity of the user is employee-2

alex-code commented 4 years ago
public function execute($user, $item, $params)
{
  Yii::debug($user);
  return isset($params['model']) ? $params['model']->id == $user : false;
}

So this logs the $user param as employee-2?

IntegrityFoundation commented 4 years ago

Yes. it logs the user as employee-4 where 4 is the id of the employee.

alex-code commented 4 years ago

Then it's not going to match, you need to remove your prefix from $user or add it to the comparison.

public function execute($user, $item, $params)
{
  return isset($params['model']) ? ('employee-' . $params['model']->id == $user) : false;
}
IntegrityFoundation commented 4 years ago

Yes it is working. Thankyou

IntegrityFoundation commented 4 years ago

But if the model is admin then the user from admin table will still access the CRUD of all groupdetails.

IntegrityFoundation commented 4 years ago

Suppose if the case is that the users in admin table are not going to access the CRUD of groupdetails. Instead they are going to access some other page then the execute method you suggested is secure and correct.

public function execute($user, $item, $params)
{
  return isset($params['model']) ? ('employee-' . $params['model']->id == $user) : false;
}
alex-code commented 4 years ago

I'm lost as to what your issue is now. I think this is better suited to the forums as it's not a framework issue.

IntegrityFoundation commented 4 years ago

Ok. Thankyou.