This post covers my notes on User Access Control (chapter 7) in Yii from the book "Web Application Development with Yii and PHP" by Jeffrey Winesett about learning Yii by taking a step-by-step approach to building a Web-based project task tracking system from conception through production deployment - software development life cycle (SDLC) issue-management application.

  • RBAC

Yii provides both a simple access control filter as well as a more sophisticated Role Based Access Control (RBAC) implementation as a means to help us address our user authorization requirements. Three roles: project owner has all administrative access to the project, project member has some administrative access, project reader has read-only access.

There are two methods relevant to access control in the ProjectController class: filters() and accessRules().

Access rules can be defined using a number of context parameters:

  • Controllers: Specifies an array of controller IDs to which the rule should apply.
  • Roles: Specifies a list of authorization items (roles, operations, and permissions) to which the rule applies.This makes use of the RBAC feature we will be discussing in the next section.
  • IPs: Specifies a list of client IP addresses to which this rule applies.
  • Verbs: Specifies which HTTP request types (GET, POST, and so on) apply to this rule.
  • Expression: Specifies a PHP expression whose value indicates whether or not the rule should be applied.
  • Actions: Specifies the action method, by use of the corresponding action ID, to which the rule should match.
  • Users: Specifies the users to which the rule should apply. The current application user's name attribute is used for matching: *: any user, ?: anonymous users, @: authenticated users. If no users are specified, the rule will apply to all users. The access rules are evaluated one by one in the order they are specified.

Ee could make changes, three times, in each of our project, issue, and user controller class files. Or add the necessary method to our base controller class. Open up protected/components/Controller.php and add the following method:

public function accessRules()
    {
        return array(
            array('allow',  // allow all users to perform 'index' and 'view' actions
                'controllers'=>array('issue','project','user'),
                'actions'=>array('index','view', 'addUser'),
                'users'=>array('@'),
            ),
            array('allow', // allow authenticated user to perform 'create' and 'update' actions
                'controllers'=>array('issue','project','user'),
                'actions'=>array('create','update'),
                'users'=>array('@'),
            ),
            array('allow', // allow admin user to perform 'admin' and 'delete' actions
                'controllers'=>array('issue','project','user'),
                'actions'=>array('admin','delete'),
                'roles'=>array('admin'),
            ),
            array('deny',  // deny all users
                'controllers'=>array('issue','project','user'),
                'users'=>array('*'),
            ),
        );
    }

Before we can establish an authorization hierarchy, assign users to roles, and perform access permission checking, we need to configure the authorization manager application component, authManager. This component is responsible for storing the permission data and managing the relationships between permissions. It also provides the methods to check whether or not a user has access to perform a particular operation. Yii provides two types of authorization managers CPhpAuthManager and CDbAuthManager. CPhpAuthManager uses a PHP script file to store the authorization data. CDbAuthManager, stores the authorization data in a database. The authManager is configured as an application component. Configuring the authorization manager consists simply of specifying which of these two types to use and then setting its initial class property values.

If use the database implementation for application. in main configuration file, protected/config/main.php, and add the following to the application components array:

// application components
'components'=>array('authManager'=>array(
'class'=>'CDbAuthManager',
'connectionID'=>'db',
),

CDbAuthManager class uses database tables to store the permission data. It expects a specific schema. That schema is identified in the framework file YiiRoot/framework/web/auth/schema.sql. It is a simple, yet elegant, schema consisting of three tables, AuthItem, AuthItemChild, and AuthAssignment.

Run

yiic migrate create create_rbac_tables

This will gen:

<?php
 
class m120619_015239_create_rbac_tables extends CDbMigration
{
    public function up()
    {
        //create the auth item table
        $this->createTable('tbl_auth_item', array(
            'name' => 'var char(64) NOT NULL',
            'type' => 'integer NOT NULL',
 
            'description' => 'text',
            'bizrule' => 'text',
            'data' => 'text',
            'PRIMARY KEY (`name`)',
        ), 'ENGINE=InnoDB');
 
        //create the auth item child table
        $this->createTable('tbl_auth_item_child', array(
            'parent' => 'var char(64) NOT NULL',
            'child' => 'var char(64) NOT NULL',
            'PRIMARY KEY (`parent`,`child`)',
        ), 'ENGINE=InnoDB');
 
        //the tbl_auth_item_child.parent is a reference to tbl_auth_item.name 
        $this->addForeignKey("fk_auth_item_child_parent", "tbl_auth_item_child", "parent", "tbl_auth_item", "name", "CASCADE", "CASCADE");
 
        //the tbl_auth_item_child.child is a reference to tbl_auth_item.name 
        $this->addForeignKey("fk_auth_item_child_child", "tbl_auth_item_child", "child", "tbl_auth_item", "name", "CASCADE", "CASCADE");
 
        //create the auth assignment table
        $this->createTable('tbl_auth_assignment', array(
            'itemname' => 'var char(64) NOT NULL',
            'userid' => 'int(11) NOT NULL',
            'bizrule' => 'text',
            'data' => 'text',
            'PRIMARY KEY (`itemname`,`userid`)',
        ), 'ENGINE=InnoDB');
 
        //the tbl_auth_assignment.itemname is a reference 
        //to tbl_auth_item.name 
        $this->addForeignKey(
            "fk_auth_assignment_itemname", 
            "tbl_auth_assignment", 
            "itemname", 
            "tbl_auth_item", 
            "name", 
            "CASCADE", 
            "CASCADE"
        );
 
        //the tbl_auth_assignment.userid is a reference 
        //to tbl_user.id 
        $this->addForeignKey(
            "fk_auth_assignment_userid", 
            "tbl_auth_assignment", 
            "userid", 
            "tbl_user", 
            "id", 
            "CASCADE", 
            "CASCADE"
        );
 
    }
 
    public function down()
    {
        $this->truncateTable('tbl_auth_assignment');
        $this->truncateTable('tbl_auth_item_child');
        $this->truncateTable('tbl_auth_item');
        $this->dropTable('tbl_auth_assignment');
        $this->dropTable('tbl_auth_item_child');
        $this->dropTable('tbl_auth_item');
    }
 
}

Open up /protected/config/main.php, and add the table name specification to the authManager component:

// application components
'components'=>array('authManager'=>array(
'class'=>'CDbAuthManager',
'connectionID'=>'db',
'itemTable' =>'tbl_auth_item',
'itemChildTable' =>'tbl_auth_item_child',
'assignmentTable' =>'tbl_auth_assignment',
),

The following diagram displays the basic hierarchy we define:

following diagram

As an example of using the API, the following code creates a new role and a new operation, and then adds the relationship between the role and the permission:

$auth=Yii::app()->authManager;
$role=$auth->createRole('owner');
$auth->createOperation('createProject','create a new project');
$role->addChild('createProject');

The RBAC framework in Yii does not have anything built-in that we can take advantage of to meet this requirement. The RBAC model is only intended to establish relationships between roles and permissions. It does not know (nor should it) anything about our TrackStar projects. In order to achieve this extra dimension to our authorization hierarchy, we need to alter our database structure to contain an association between a user, project, and role.

Table tbl_project_user_assignment is used to join many2many relatin between Users&Projects, add Roles in it:

<?php
 
class m120620_020255_add_role_to_tbl_project_user_assignment extends CDbMigration
{
    public function up()
    {
        $this->addColumn('tbl_project_user_assignment', 'role', 'var char(64)');
        //the tbl_project_user_assignment.role is a reference to tbl_auth_item.name 
        $this->addForeignKey('fk_project_user_role', 'tbl_project_user_assignment', 'role', 'tbl_auth_item', 'name', 'CASCADE', 'CASCADE');
    }
 
    public function down()
    {
        $this->dropForeignKey('fk_project_user_role', 'tbl_project_user_assignment');
        $this->dropColumn('tbl_project_user_assignment', 'role');
    }
 
}

We need to add the public method to the Project AR class that will take in a role name and a user ID and create the association between role, user, and project. In protected/models/Project.php file and add the following method:

/**
     * Assigns a user, in a specific role, to the project
     * @param int $userId the primary key for the user
     * @param string $role the role assigned to the user for the project    
     */
    public function assignUser($userId, $role)
    {
        $command = Yii::app()->db->createCommand();
        $command->insert('tbl_project_user_assignment', array(
            'role'=>$role,
            'user_id'=>$userId,
            'project_id'=>$this->id,
        ));
    }
    /**
     * Removes a user from being associated with the project
     * @param int $userId the primary key for the user
     */
    public function removeUser($userId)
    {
        $command = Yii::app()->db->createCommand();
        $command->delete('tbl_project_user_assignment', 'user_id=:userId AND project_id=:projectId', array(':userId'=>$userId,':projectId'=>$this->id));
    }
    /**
     * Determines whether or not the current application user is in the role for the project
     * @param string $role the role assigned to the user for the project 
     * @return boolean whether or not the user is in the role for this project  
     */
    public function allowCurrentUser($role)
    {
        $sql = "S E L E C T * FROM tbl_project_user_assignment WHERE project_id=:projectId AND user_id=:userId AND role=:role";
        $command = Yii::app()->db->createCommand($sql);
        $command->bindValue(":projectId", $this->id, PDO::PARAM_INT);
        $command->bindValue(":userId", Yii::app()->user->getId(), PDO::PARAM_INT);
 
 
        $command->bindValue(":role", $role, PDO::PARAM_STR);
        return $command->e xecute()==1 ? true : false;
    }

Adding users to projects:

  • Add a public static method called getUserRoleOptions() to the Project model class that returns a valid list of role options using the auth manager's getRoles() method.
  • Add a new public method called isUserInProject($user) to the Project model class to determine if a user is already associated with a project.
  • Add a new form model class called ProjectUserForm, extending from CFormModel for a new input form model. Add to this form model class three attributes, namely $username, $role, and $project. Also add validation rules to ensure that both the username and the role are required input fields, and that the username should further be validated via a custom verify() class method. This verify method should attempt to create a new UserAR class instance by finding a user matching the input username. If the attempt was successful, it should continue to associate the user to a project using the assignUser($userId, $role)method. Also associate the user to the role in our RBAC hierarchy implemented earlier in this chapter. If no user was found matching the username, it needs to set and return an error. (If needed, review the LoginForm::authenticate()method as an example of a custom validation rule method.)
  • Add a new view file under views/project called adduser.php to display our new form for adding users to projects. This form only needs two input fields, username and role. The role should be a drop-down choice listing.
  • Add a new controller action method called actionAdduser() to the ProjectController class and alter its accessRules() method to ensure it is accessible by authenticated members. This new action method is responsible for rendering the new view to display the form and handle the post back when the form is submitted.
//Projects.php - model add:
    /**
     * Returns an array of available roles in which a user can be placed when being added to a project
     */
    public static function getUserRoleOptions()
    {
        return CHtml::listData(Yii::app()->authManager->getRoles(), 'name', 'name');    
    } 
 
    /* 
     * Determines whether or not a user is already part of a project
     */
    public function isUserInProject($user) 
    {
        $sql = "SELECT user_id FROM tbl_project_user_assignment WHERE project_id=:projectId AND user_id=:userId";
        $command = Yii::app()->db->createCommand($sql);
        $command->bindValue(":projectId", $this->id, PDO::PARAM_INT);
        $command->bindValue(":userId", $user->id, PDO::PARAM_INT);
        return $command->e xecute()==1;
    }
 
//ProjectUserForm.php
<?php
/**
 * ProjectUserForm class.
 * ProjectUserForm is the data structure for keeping
 * the form data related to adding an existing user to a project. It is used by the 'Ad-duser' action of 'ProjectController'.
 */
class ProjectUserForm extends CFormModel
{
    /**
     * @var string username of the user being added to the project
     */
    public $username;
 
    /**
     * @var string the role to which the user will be associated within the project
     */
    public $role; 
 
    /**
     * @var object an instance of the Project AR model class
     */ 
    public $project;
 
    private $_user;
 
    /**
     * Declares the validation rules.
     * The rules state that username and password are required,
     * and password needs to be authenticated using the verify() method
     */
    public function rules()
    {
        return array(
            // username and role are required
            array('username, role', 'required'),
            //username needs to be checked for existence 
            array('username', 'exist', 'className'=>'User'),
            array('username', 'verify'),
        );
    }
 
 
    /**
     * Authenticates the existence of the user in the system.
     * If valid, it will also make the association between the user, role and project
     * This is the 'verify' validator as declared in rules().
     */
    public function verify($attribute,$params)
    {
        if(!$this->hasErrors())  // we only want to authenticate when no other input errors are present
        {
            $user = User::model()->findByAttributes(array('username'=>$this->username));
            if($this->project->isUserInProject($user))
            {
                $this->addError('username','This user has already been added to the project.'); 
            }
            else
            {
                $this->_user = $user;
            }
        }
    }
 
    public function assign()
    {
        if($this->_user instanceof User)
        {
 
            //assign the user, in the specified role, to the project
            $this->project->assignUser($this->_user->id, $this->role);  
            //add the association, along with the RBAC biz rule, to our RBAC hierarchy
            $auth = Yii::app()->authManager; 
            if(!$auth->isAssigned($this->role, $this->_user->id))
            {
                $bizRule='return isset($params["project"]) && $params["project"]->allowCurrentUser("'.$this->role.'");';  
                $auth->assign($this->role,$this->_user->id, $bizRule);
            }
            return true;
        }
        else
        {
            $this->addError('username','Error when attempting to assign this user to the project.'); 
            return false;
        }
 
    }
 
    /**
     * Generates an array of usernames to use for the autocomplete
     */
    public function createUsernameList()
    {
        $sql = "SELECT username FROM tbl_user";
        $command = Yii::app()->db->createCommand($sql);
        $rows = $command->queryAll();
        //format it for use with auto complete widget
        $usernames = array();
        foreach($rows as $row)
        {
            $usernames[]=$row['username'];
        }
        return $usernames;
 
    }
}
 
//ProjectControoller - addUser:
    /**
     * Provides a form so that project administrators can
     * associate other users to the project
     */
    public function actionAdduser($id)
    {
        $project = $this->loadModel($id);
        if(!Yii::app()->user->checkAccess('createUser', array('project'=>$project)))
        {
            throw new CHttpException(403,'You are not authorized to perform this action.');
        }
 
        $form=new ProjectUserForm; 
        // collect user input data
        if(isset($_POST['ProjectUserForm']))
        {
            $form->attributes=$_POST['ProjectUserForm'];
            $form->project = $project;
            // validate user input  
            if($form->validate())  
            {
                if($form->assign())
                {
                    Yii::app()->user->setFlash('success',$form->username . " has been added to the project." ); 
                    //reset the form for another user to be associated if desired
                    $form->unsetAttributes();
                    $form->clearErrors();   
                }
            }
        }
        $form->project = $project;
        $this->render('adduser',array('model'=>$form)); 
    }
 
//protected/views/project/adduser.php:
<?php
$this->pageTitle=Yii::app()->name . ' - Add User To Project';
$this->breadcrumbs=array(
    $model->project->name=>array('view','id'=>$model->project->id),
    'Add User',
);
$this->menu=array(
    array('label'=>'Back To Project', 'url'=>array('view','id'=>$model->project->id)),
);
?>
 
<h1>Add User To <?php echo $model->project->name; ?></h1>
 
<?php if(Yii::app()->user->hasFlash('success')):?>
     <div class="successMessage">
          <?php echo Yii::app()->user->getFlash('success'); ?>
     </div>
<?php endif; ?>
 
<div class="form">
<?php $form=$this->beginWidget('CActiveForm'); ?>
 
    <p class="note">Fields with <span class="required">*</span> are required.</p>
 
    <div class="row">
        <?php echo $form->labelEx($model,'username'); ?>
        <?php 
        $this->widget('zii.widgets.jui.CJuiAutoComplete', array(
            'name'=>'username',
            'source'=>$model->createUsernameList(),
            'model'=>$model,
            'attribute'=>'username',
            'options'=>array(
                'minLength'=>'2',
            ),
            'htmlOptions'=>array(
                'style'=>'height:20px;'
            ),
        ));
        ?>
        <?php echo $form->error($model,'username'); ?>
    </div>
 
    <div class="row">
        <?php echo $form->labelEx($model,'role'); ?>
        <?php echo $form->dropDownList($model,'role', Project::getUserRoleOptions()); ?>
        <?php echo $form->error($model,'role'); ?>
    </div>
 
 
    <div class="row buttons">
        <?php echo CHtml::submitButton('Add User'); ?>
    </div>
 
<?php $this->endWidget(); ?>
</div>

Download source code of chapter 7

One comment

#3
Me says:
Monday, August 4, 2014 7:30 PM
We accomplish this by assigning users to one
of the three roles we created, owner, member, or reader. For example, if we wanted to associate the user whose unique user ID is 1 with the member role, we would execute the following:
$auth=Yii::app()->authManager;
$auth->assign('member',1);

Leave a Comment

Fields with * are required.

Please enter the letters as they are shown in the image above.
Letters are not case-sensitive.