《Agile Web Application Development with Yii1.1 and PHP5》

Chapter 8: Iteracion 5: User Access Control

User based web applications, like our TrackStar application, typically need to control access to certain functionality based on who is making the request. When we speak of user access control, we are referring, at a high-level, to some questions the application needs to ask when requests are being made such as:

  • Who is making the request?
  • Does that user have the appropriate permission to access the requested functionality?

The answers to these questions help the application respond appropriately.

The work completed in the last iteration provides the application with the ability to answer the first question. Our implementation of basic user management extended the application user authentication process to use the database. The application now allows users to establish their own authentication credentials and validates the username and password against the database stored values upon user login. After a successful login, the application now knows exactly who is making subsequent requests.

This iteration is going to focus on helping the application answer the second question. Once the user has provided appropriate identification, the application needs a way to determine if they also have the permission to perform the requested action. We'll extend our basic authorization model by taking advantage of Yii's user access control features. Yii provides both a simple access control filter as well as a more sophisticated role-based access control (RBAC) implementation as means to help us address our user authorization requirements. We'll be taking a closer look at both of these as we work to implement the user access requirements for the TrackStar application.

Iteration planning

When we first introduced our TrackStar application back in Chapter 3, The TrackStar Application, we mentioned that the application has two high-level user types: anonymous and authenticated. This is simply making a distinction between a user that has successfully logged in, and one who has not. We have also introduced the idea of authenticated users having different roles within a project. We established that, within a project, a user can be in one of three roles:

  • A project owner (is granted all administrative access to the project)
  • A project member (is granted more limited access to project features and functionality)
  • A project reader (only has access to read the content associated with a project, not change it in any way) >

The focus of this iteration is to implement an approach to managing the access control granted to application users. We need a way to create and manage our roles and permissions, assign them to users, and enforce the access control rules we want for each user role.

In order to achieve this goal, we need to identify all the more granular items we will work on within this iteration. The following is a list of these items:

  • Implement a strategy to force the user to log in before gaining access to any project or issue related functionality
  • Create user roles and associate those roles with a specific functionality permission structure
  • Implement the ability to assign users to roles (and their associated permissions)
  • Ensure our role and permission structure exists on a per project basis (that is, allow users to have different permissions within different projects)
  • Implement the ability to associate users to projects and, at the same time, to roles within that project
  • Implement the necessary authorization access checking throughout the application to appropriately grant or deny access to the application user based on their permissions

Luckily, Yii comes with a lot of built-in functionality to help us implement these requirements. So, let's get started.

Running our existing test suite

As always, we should kick things off by running all of our existing unit tests to ensure that the tests pass:

% cd WebRoot/protected/tests/ 
% phpunit unit/ 
PHPUnit 3.4.12 by Sebastian Bergmann. 
................ 
Time: 3 seconds 
OK (10 tests, 27 assertions)

Everything looks good, so we can start making changes.

accessControl filter

We introduced filters back in Chapter 6, Iteration 3: Adding Tasks when we added one to help us verify the project context when dealing with our Issue related CRUD operations. The Yii Framework provides a filter called accessControl. This filter can be directly used in controller classes to provide an authorization scheme to verify whether or not a user can access a specific controller action. In fact, the astute reader will remember that when we were implementing our filterProjectContext filter back in Chapter 6, we noticed that access control filter was already included in the filters list for both our IssueController and ProjectController classes, as follows:

/**
* @return array action filters
*/ 
public function filters() {
    return array( 
        'accessControl',
        // perform access control for CRUD operations 
    );
}

This was included in the autogenerated code produced by using the Gii code generator to create our skeleton CRUD operations on the Issue and Project AR classes.

The default implementation is set up to allow anyone to view a list of existing issues and projects. However, it restricts access of creating and updating to authenticated users, and further restricts the Delete action to a special admin user. You might remember that when we first implemented CRUD operations on projects, we had to log in before we were able to create new ones. The same was true when dealing with issues and again with users. The mechanism controlling this authorization and access is exactly this accessControl filter. Let's take a closer look at this implementation within the ProjectController.php class file.

There are two methods relevant to access control in this file, ProjectController::filters() and ProjectController::accessRules(). The code for the first method is listed as follows:

/** 
* @return array action filters 
*/
public function filters() 
{
    return array( 
        'accessControl', // perform access control for CRUD operations
    );
}

The following code is used for the second method:

/**
*Specifies the access control rules.
*This method is used by the 'accessControl' filter.
*@return array access control rules
*/
 
public function accessRules()
{
    return array(
        array('allow', // allow all users to perform 'index' and'view' actions
            'actions'=>array('index','view'),
            'users'=>array('*'),
        ),
 
        array('allow', // allow authenticated user to perform 'create' and 'update' actions
            'actions'=>array('create','update'),
            'users'=>array('@'),
        ),
 
        array('allow', // allow admin user to perform 'admin' and 'delete' actions
            'actions'=>array('admin','delete'), 
            'users'=>array('admin'),
        ), 
 
        array('deny', // deny all users
            'users'=>array('*'),
        ),
    );
}

The filters() method is already familiar to us. It is where we specify all the filters to be used in the controller class. In this case, we have only one, accessControl, which refers to a filter provided by the Yii Framework. This filter uses the other method, accessRules(), which defines the rules that drive the access restrictions.

In the accessRules() method mentioned previously, there are four rules specified. Each rule is represented as an array. The first element of the array is either allow or deny. These indicate the granting or denying of access respectively. The rest of the array consists of name=>value pairs specifying the remaining parameters of the rule.

Let's look at the first rule defined previously:

array('allow', // allow all users to perform 'index' and 'view' actions
    'actions'=>array('index','view'), 
    'users'=>array('*'),
),

This rule allows the index and view controller actions to be executed by any user. The asterisk '*' special character is a way to specify any user (anonymous, authenticated, or otherwise).

The second rule is as follows:

array('allow', // allow authenticated user to perform 'create' and 'update' actions
    'actions'=>array('create','update'), 
    'users'=>array('@'),
),

It allows for any authenticated user to access the create and update controller actions. The '@' special character is a way to specify any authenticated user.

The third rule is as follows:

array('allow', // allow admin user to perform 'admin' and 'delete' actions
    'actions'=>array('admin','delete'), 
    'users'=>array('admin'),
),

This specifies that a specific user, named admin, is allowed to access the actionAdmin() and actionDelete() controller actions.

The fourth rule is as follows:

array('deny', // deny all users 
    'users'=>array('*'),
),

It denies access to all controller actions to all users.

Access rules can be defined using a number of context parameters. The previously mentioned rules define actions and users to create the rule context, but there are several others listed as follows:

  • Controllers: This rule specifies an array of controller IDs to which the rule should apply.
  • Roles: This rule specifies a list of authorization items (roles, operation, permissions) to which the rule applies. This makes used of the RBAC feature we will be discussing in the next section.
  • Ips: This rule specifies a list of client IP addresses to which this rule applies.
  • Verbs: This rule specifies which HTTP request types (GET, POST, and so on) apply to this rule.
  • Expression: This rule specifies a PHP expression whose value indicates whether or not the rule should be applied.
  • Actions: This rule specifies the action method, by use of the corresponding action ID, to which the rule should match.
  • Users: This rule specifies the users to which the rule should apply. The current application user's name attribute is used for matching. Three special characters can also be used here:
    • any user
    • ?: anonymous users
    • @: authenticated users

The access rules are evaluated one by one in the order by which they are specified. The first rule that matches the current pattern determines the authorization result. If this rule is an allow rule, the action can be executed; if it is a deny rule, the action cannot be executed; if none of the rules matches the context, the action can still be executed. It is for this reason that the fourth rule is stipulated. If we did not stipulate a rule that denied all actions to all users at the end of our rules list, then we would not achieve our desired access restrictions. As an example, take the second rule. It specifies that authenticated users are allowed access to the create and update actions. However, it does not stipulate that anonymous users be denied access. It says nothing about anonymous users. The fourth rule ensures that all other requests that do not match one of the first three specific rules be denied access.

With this already in place, altering our application to deny anonymous users access to all project, issue, and user related functionality is a snap. All we have to do is change the special character '*' of the users array value to the '@' special character. This will only allow authenticated users to access the actionIndex() and actionView() controller actions. All other actions are already restricted to authenticated users.

Let's make this change in all of our controllers. Open up all three of the following files: ProjectController.php, IssueController.php, and UserController.php files and alter the first rule in the access control rules to be:

array('allow', // allow only authenticated users to perform 'index' and 'view' actions
    'actions'=>array('index','view'), 
    'users'=>array('@'),
),

After making these changes, the application will require a login prior to accessing any of our project, issue, or user functionality. We still allow anonymous user access to the SiteController class action methods, which we kept because this is where our login actions are located. We have to be able to access the login page if we are not already logged in.

Role-based access control

Now that we have used the simple accessControl filter as a broad stroke to limiting access to authenticated users, we need to turn focus to meeting some more granular access control needs of our application. As we mentioned, users will play certain roles within a project. The project will have users of type owner, who can be thought of as project administrators. They will be granted all access to manipulate the project. The project will also have users of type member, who will be granted some access to project functionality, but a subset of what owners are able to perform. Finally, the project can have users of type reader, who are only able to view project related content and not alter it in any way. To achieve this type of access model based on the role of a user, we turn to the RBAC feature of Yii.

RBAC is an established approach in computer systems security to managing the access permissions of authenticated users. In short, the RBAC approach defines roles within an application. Permissions to perform certain operations are also defined and then associated with roles. Users are then assigned to a role and through the role association, acquire the permissions defined for that role. There is plenty of documentation available for curious readers about the general RBAC concept and approach. One good source of information is Wikipedia: http://en.wikipedia. org/wiki/Role-based_access_control. We'll focus on the specifics of Yii's implementation of RBAC.

Yii's implementation of RBAC is simple, elegant, and powerful. At the foundation of RBAC in Yii is the idea of the authorization item. The authorization item is simply a permission to do things in the application. These permissions can be categorized as roles, tasks, or operations, and, as such, form a permission hierarchy. Roles can consist of tasks (or other roles), tasks can consist of operations (or other tasks) and operations are the most granular permission level.

For example, in our TrackStar application, we need a role of type owner. So, we would create an authorization item of type role with the name owner. This role could then consist of tasks such as a "user management" and "issue management". These tasks could then further consist of the atomic operations that make up these tasks. For example, the user management task could consist of the operations create new user, edit user, and delete user. This hierarchy allows for inheritance of these permissions so that, given this example, if a user is assigned to the owner role, they inherit the permission to perform create, edit, and delete user operations.

Typically in RBAC, you assign a user to one or more roles and the user inherits the permissions that have been assigned to those roles. This holds true for RBAC in Yii as well. However, in this model, we can associate users to any authorization item, not just ones of type role. This allows us the flexibility to associate a permission to a user at any level of granularity. If we only want to grant the delete user operation to a specific user, and not give them all the access that an owner role would have, we can simply associate the user to this atomic operation. This makes RBAC in Yii very flexible.

Configuring the authorization manager

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 as well as providing the methods to check whether or not a user does have 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, as you might have guessed, 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.

As we are already using a database in the TrackStar application, it makes sense for us to make use of the CDbAuthManager implementation. To make this configuration, open up the main config file, protected/config/main.php, and add the following to the application components array:

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

This establishes a new application component named authManager, specifies the class type to be CDbAuthManager, and sets the connectionID class property to be our database connection component. Now we can access this anywhere in our application using Yii::app()->authManager.

Creating the RBAC database tables

As mentioned, the 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. The AuthItem table holds the information defining the authorization item, that is the role, task or operation. The AuthItemChild table houses the parent/child relationships that form our hierarchy of authorization items. Finally, the AuthAssignment table is an association table that holds the association between a user and an authorization item. The basic DDL statements for the tables are the following:

create table AuthItem 
( 
    name    varchar(64) not null,
    type    integer not null,
    description    text,
    bizrule    text,
    data    text,
    primary key (name)
);
 
create table AuthItemChild 
(
    parent    varchar(64) not null,
    child    varchar(64) not null,
    primary key (parent,child),
    foreign key (parent) references AuthItem (name) on delete cascade on update cascade,
    foreign key (child) references AuthItem (name) on delete cascade on update cascade 
);
 
create table AuthAssignment 
(
    itemname    varchar(64) not null,
    userid    varchar(64) not null,
    bizrule    text,
    data    text,
    primary key (itemname,userid),
    foreign key (itemname) references AuthItem (name) on delete cascade on update cascade
);

This schema is taken directly from the Yii Framework file /framework/ web/auth/schema.sql and does not exactly adhere to our table naming conventions that we use for our other tables. These are the default table names expected by CDbAuthManager class. However, you can configure this class to use different table names. For simplicity, we use the schema exactly as defined in the framework.

Creating the RBAC authorization hierarchy

After adding the previously mentioned tables to our _dev and _test databases, we need to populate them with our roles and permissions. We will do this using the API provided by the authManager. To keep things simple, we are going to only define roles and basic operations. We will not set up any formal RBAC tasks for now. The following figure displays the basic hierarchy we wish to define:

The diagram shows inheritance from the top down. So, Owners have all the permissions listed, plus they inherit all the permissions from both the Member and Reader roles. Likewise, member inherits permissions from the Reader. What we now need to do is establish this permission hierarchy in the application. As previously mentioned, the best way to do this is to write code to utilize the authManager API. As an example, 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');

In the preceding code, we first get an instance of the authManager. We then use its createRole(), createOperation(), and addChild() API methods to create a new owner role, and a new operation named createProject. We then add the permission to the owner role. This only demonstrates the creation of a small part of our needed hierarchy, all of the remaining relationships we outlined in the previous figure need to be created in a similar manner.

To accomplish the building of our needed permission hierarchy, we are going to write a simple shell command, which is to be executed at the command line. This will extend the command options of the yiic command-line tool we used to create our initial application.

Writing a console application command

We introduced the yiic command-line tool back in Chapter 2, when we created a new HelloWorld! application, and again in Chapter 4 when we used it to initially create the structure of our TrackStar Web application. The yiic tool is a console application in Yii that executes tasks in the form of commands. We have used the webapp command to create a new applications, and back in Chapter 2, we also used the yiic shell command to create a new controller class. We have been using the newer Gii code generator tool when initially creating our model classes and our CRUD scaffolding code. However, there are commands available with the yiic tool for creating these as well. As a reminder, the yiic shell command allows you to interact with a web application on the command line. You can execute it from the folder that contains the entry script for the application. Then, within the context of the specific application, it provides tools to automatically generate new controllers, views and data models.

Console applications in Yii are easily extended by writing custom commands, and this is exactly what we are going to do. We are going to extend the yiic shell command tool set by writing a new command-line tool to allow us to build our RBAC authorization hierarchy in a consistent and repeatable manner.

Writing a new command for a console application is quite simple. It is simply a class that extends from CConsoleCommand which, at a minimum, implements the needed run() method that will be executed when the command is called. The name of the class should be exactly the same as the desired command name, followed by Command. In our case, our command will simply be rbac, so we'll name our class RbacCommand. Lastly, in order to make this command available to the yiic console application, we need to save our class into the /protected/commands/shell/ folder.

So, create a new file called RbacCommand.php, and add the following PHP code:

<?php 
class RbacCommand extends CConsoleCommand
{
    private $_authManager;
 
    public function getHelp()
    {
        return <<<EOD
USAGE
  rbac
DESCRIPTION
  This command generates an initial RBAC authorization hierarchy.
EOD;
    }
 
    /**
     * Execute the action.
     * @param array command line parameters specific for this command
     */
    public function run($args)
    {
        //ensure that an authManager is defined as this is mandatory for creating an auth heirarchy
        if(($this->_authManager=Yii::app()->authManager)===null)
        {
            echo "Error: an authorization manager, named 'authManager' must be configured to use this command.\n";
            echo "If you already added 'authManager' component in application configuration,\n";
            echo "please quit and re-enter the yiic shell.\n";
            return;
        }
 
        //provide the opportunity for the use to abort the request
        echo "This command will create three roles: Owner, Member, and Reader and the following premissions:\n";
        echo "create, read, update and delete user\n"; echo "create, read, update and delete project\n";
        echo "create, read, update and delete issue\n"; echo "Would you like to continue? [Yes|No] ";
 
        //check the input from the user and continue if they indicated yes to the above question
        if(!strncasecmp(trim(fgets(STDIN)),'y',1))
        {
            //first we need to remove all operations, roles, child relationship and assignments
            $this->_authManager->clearAll();
 
            //create the lowest level operations for users
            $this->_authManager->createOperation("createUser","create a new user");
            $this->_authManager->createOperation("readUser","read user profile information");
            $this->_authManager->createOperation("updateUser","update a users information");
            $this->_authManager->createOperation("deleteUser","remove a user from a project");
 
            //create the lowest level operations for projects
            $this->_authManager->createOperation("createProject","create a new project");
            $this->_authManager->createOperation("readProject","read project information");
            $this->_authManager->createOperation("updateProject","update project information");
            $this->_authManager->createOperation("deleteProject","delete a project");
 
            //create the lowest level operations for issues
            $this->_authManager->createOperation("createIssue","crea te a new issue");
            $this->_authManager->createOperation("readIssue","read issue information");
            $this->_authManager->createOperation("updateIssue","upda te issue information");
            $this->_authManager->createOperation("deleteIssue","dele te an issue from a project");
 
            //create the reader role and add the appropriate permissions as children to this role
            $role=$this->_authManager->createRole("reader");
            $role->addChild("readUser");
            $role->addChild("readProject");
            $role->addChild("readIssue");
 
            //create the member role, and add the appropriate permissions, as well as the reader role itself, as children
            $role=$this->_authManager->createRole("member"); $role->addChild("reader");
            $role->addChild("createIssue");
            $role->addChild("updateIssue");
            $role->addChild("deleteIssue");
 
            //create the owner role, and add the appropriate permissions, as well as both the reader and member roles as children
            $role=$this->_authManager->createRole("owner");
            $role->addChild("reader");
            $role->addChild("member");
            $role->addChild("createUser");
            $role->addChild("updateUser");
            $role->addChild("deleteUser");
            $role->addChild("createProject");
            $role->addChild("updateProject");
            $role->addChild("deleteProject");
 
            //provide a message indicating success 
            echo "Authorization hierarchy successfully generated.";
        }
    }
}

The comments in the previous code should help tell the story of what is happening here. We provide a simple getHelp() method so that our new command can be quickly understood by other users. This is also consistent with the other commands offered by yiic. All of the real action happens in the run() method. It ensures the application has a vaild authManager application component defined. It then allows the user to have a last chance to cancel the request before proceeding. If the user of this command indicates they want to continue, it will proceed to clear all previously entered data in the RBAC tables and then create a new authorization hierarchy. The hierarchy that is created here is exactly the one we discussed previously.

We can see that, even based on our fairly simple hierarchy, there is still a significant amount of code needed. Typically, one would need to develop a more intuitive UI wrapped around these authorization manager APIs to provide an easy interface to manage roles, tasks, and operations. For the purposes of our TrackStar application, we can simply set up the needed database tables, execute this logic once to establish the initial relationships, and then hope we don't have to make too many changes to it. This is a great solution for establishing a quick RBAC permission structure, but not ideal for the long-term maintenance of a permission structure that might change significantly.

In a real-world application, you will most likely need a different, more interactive tool to help maintain the RBAC relationships. The Yii extension library (http://www.yiiframework.com/ extensions/) provides some packaged solutions for this.

Let's try out this new command. Navigate to the root of your application and execute the shell command (Remember YiiRoot stands for where you have installed the Yii Framework):

% YiiRoot/framework/yiic shell 
Yii Interactive Tool v1.1 (based on Yii v1.1.2) 
Please type 'help' for help. Type 'exit' to quit. 
>>

Now type help to see a list of available commands:

At the prompt, you may enter a PHP statement or one of the following commands:
  - controller
  - crud
  - form
  - help
  - model
  - module
  - rbac
Type 'help <command-name>' for details about a command.

We see that our rbac command has now been added to the list. Let's attempt to learn more by typing help rbac:

>> help rbac
USAGE
    rbac
 
DESCRIPTION
    This command generates an initial RBAC authorization hierarchy.

This is exactly what we wrote in the getHelp() method of our command class. You can certainly be more verbose, and add more detail as desired.

Now let's run the command to establish the required hierarchy:

>> rbac
This command will create three roles: Owner, Member, and Reader and the following premissions:
create, read, update and delete user
create, read, update and delete project
create, read, update and delete issue
Would you like to continue? [Yes|No] Yes
Authorization hierarchy successfully generated.

Then go ahead and exit the shell:

>> exit

Assuming you typed Yes when prompted to continue, all of the authorization hierarchy was created.

As you may recall, we have setup a separate database to run our tests against, namely trackstar_test. As we will need this authorization hierarchy in our test database as well, we need to run the yiic shell command under the context of the TrackStar application pointed to the test database. As our test database connection string is defined in our test config file, /protected/config/test.php, we need to bootstrap the yiic shell with this config file rather than main.php. This is easy to do, as the yiic shell command allows you to explicitly specify a config file to load. So, let's once again start the yiic shell, but let's specify our test configuration when starting up so that the interactive web application shell is configured to use our test database:

% YiiRoot/framework/yiic shell protected/config/test.php
Yii Interactive Tool v1.1 (based on Yii v1.1.2)
Please type 'help' for help. Type 'exit' to quit.
>> rbac
This command will create three roles: Owner, Member, and Reader and the following premissions:
create, read, update and delete user
create, read, update and delete project
create, read, update and delete issue
Would you like to continue? [Yes|No] Yes
Authorization hierarchy successfully generated.
>> exit

Now we have our RBAC authorization hierarchy available in our test database as well.

Assigning users to roles

Everything we have done thus far does establish an authorization hierarchy, but it does not yet assign permissions to users. 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);

Once these relationships are established, checking a user's access permission is a simple matter. We simply ask the application user component whether or not the current user has the permission. For example, if we wanted to check whether or not the current user is allowed to create a new issue, we could do so with the following syntax:

if( Yii::app()->user->checkAccess('createIssue')) {
    //perform needed logic
}

In this example, we assigned user ID 1 to the role of member, and as in our authorization hierarchy the member role inherits the createIssue permission, the previously mentioned if statement would evaluate to true, assuming we were logged in to the application as user 1.

We will be adding this authorization assignment logic as part of the business logic executed when adding a new member to a project. We'll be adding a new form that allows us to add users to projects, and the ability to choose a role as part of the process. But first we need to address one other aspect of how user roles need to be implemented within this application, namely that they need to apply on a per project basis.

Adding RBAC roles to projects

We now have a basic RBAC authorization model in place, but these relationships apply to the application as a whole. Our needs for the TrackStar application are slightly more complex. We need to define roles within the context of projects, not just globally across the application. We need to allow users to be in different roles, depending on the project. For example, a user may be in the reader role of one project, a member of a second project, and an owner of some third project. Users can be associated with many projects, and the role they are assigned needs to be specific to the project.

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 will create a separate database table to maintain the relationship between a user, a role and a project. The DDL statement for this table is as follows:

create table tbl_project_user_role 
(
    project_id INTEGER NOT NULL, 
    user_id INTEGER NOT NULL, 
    role VARCHAR(64) NOT NULL, 
    primary key (project_id,user_id,role), 
    foreign key (project_id) references tbl_project (id), 
    foreign key (user_id) references tbl_user (id), 
    foreign key (role) references AuthItem (name)
);

So, open your favorite database editor and ensure this table is part of both the main and test database models.

Adding RBAC business rules

Although the previous database table will hold the basic information to answer the question as to whether a user is assigned to a role within the context of a particular project, we still need our RBAC authorization hierarchy to answer questions concerning whether or not a user has permission to perform certain functionality. Although the RBAC model in Yii does not know about our TrackStar projects, it does have a very powerful feature that we can take advantage of. When you create authorization items or assign an item to a user, you can associate a snippet of PHP code that will be executed during the Yii::app()->user->checkAccess() call. When defined, this bit of code must return true before the user would be granted that permission.

One example of the usefulness of this feature is in the context of applications that allow users to maintain personal profile information. Often in this case, the application would like to ensure that a user have the permission to update only their own profile information and no one else's. In this case we could create an authorization item called updateProfile, and then associate a business rule that checks if the current user's ID is the same as the user ID associated with the profile information.

In our case, we are going to associate a business rule with the role assignment. When we assign a user to a specific role, we will also associate a business rule that will check the relationship within the context of the project. The checkAccess() method also allows us to pass in an array of additional parameters for the business rule to use to perform its logic. We'll use this to pass in the current project context so that the business rule can call a method on the Project AR class to determine whether or not the user is assigned to that role within that project.

The business rule we'll create will be slightly different for each role assignment. For example, the one we'll use when assigning a user to the owner role will look like the following:

$bizRule='return isset($params["project"]) && $params["project"]- >isUserInRole('owner');';

The ones for member and reader will be the similar.

We will also have to pass in the project context when we call the checkAccess() method. So now when checking if a user has access to, for example, the createIssue operation, the code would look like:

$params=array('project'=>$project); 
if(Yii::app()->user->checkAccess('createIssue',$params)) 
{
    //proceed with issue creation logic
}

Here, the $project variable is the Project AR class instance associated with the current project context (remember that almost all functionality in our application occurs within the context of a project).

Implementing the new Project AR methods

Now that we have added a new database table to house the relationship between user, role and project, we need to implement the required logic to manage and verify the data in this table. We will be adding public methods to the Project AR class to handle adding and removing data from this table, as well as verifying the existence of rows. As you may have guessed, we will start by writing a test.

First, let's add the ability to create a new association between a user, project and role. Open up the unit test file protected/tests/unit/ProjectTest.php, and add the following test:

public function testUserRoleAssignment() 
{
    $project = $this->projects('project1'); 
    $this->assertEquals(1,$project->associateUserToRole());
}

and then run the following test:

% cd /Webroot/protected/tests/ 
% phpunit unit/ProjectTest.php 
PHPUnit 3.4.12 by Sebastian Bergmann. 
.....E 
Time: 0 seconds 
There was 1 error: 
1) ProjectTest::testUserRoleAssignment 
CException: Project does not have a method named "associateUserToRole". 
... FAILURES! 
Tests: 6, Assertions: 13, Errors: 1.

We have our test failing, and for obvious reasons. 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. Open up the protected/models/ Project.php file and add the following method with just enough logic to get the test to pass:

/** 
  * creates an association between the project, the user and the user's role within the project 
  */
public function associateUserToRole()
{
    return 1;
}

Running the test again will result in success, as we have simply returned exactly what test is looking to compare:

% phpunit unit/ProjectTest.php 
PHPUnit 3.4.12 by Sebastian Bergmann. 
...... 
Time: 0 seconds 
OK (6 tests, 14 assertions)

Now let's alter the test to pass in the role name and the user ID to the method on the project class:

public function testUserRoleAssignment() 
{
    $project = $this->projects('project1'); 
    $user = $this->users('user1'); 
    $this->assertEquals(1,$project->associateUserToRole('owner', $user->id)); 
}

Then alter the Project::associateUserToRole() method to take in these parameters, and actually insert a row into our tbl_project_user_role table:

public function associateUserToRole($role, $userId) 
{
    $sql = "INSERT INTO tbl_project_user_role (project_id, user_id, role) VALUES (:projectId, :userId, :role)";
    $command = Yii::app()->db->createCommand($sql); 
    $command->bindValue(":projectId", $this->id, PDO::PARAM_INT); 
    $command->bindValue(":userId", $userId, PDO::PARAM_INT); 
    $command->bindValue(":role", $role, PDO::PARAM_STR);
    return $command->execute();
}

Here we are using the Yii Framework CDbCommand class to execute an SQL statement against the database. An instance of CDbCommand is what is returned from calling the createCommand() method on our database connection. We are also using binding our parameter values using the bindValue() method on the CDbCommand. This is a good practice which can reduce the risk of SQL injection attacks as well as help improve the performance of SQL statements that are executed multiple times.

The CDbCommand::execute() method used previously returns the number of rows affected by the executed SQL insert statement. A successful insert will affect one row, so the integer value 1, will be returned. The test compares the return value of this execution to the integer 1. If you are following along, you should verify that the test does pass. However, if you run it a second time, it will fail with a database integrity constraint violation, as it will be trying to insert the same Primary key again. We should take a moment to address this issue.

As we are dealing with a database table in our tests, we should really add a fixture for this table to be able to run our tests in a repeatable and consistent manner.

Add a new file called tbl_project_user_role.php to the fixtures folder, protected/tests/fixtures/, and have it simply return a blank array:

<?php 
return array( 
);

Next, alter the fixtures array at the top of the protected/tests/unit/Project- Test.php file to include this new fixture:

public $fixtures=array( 
    'projects'=>'Project',
    'users'=>'User', 
    'projUsrAssign'=>':tbl_project_user_assignment', 
    'projUserRole'=>':tbl_project_user_role', 
);

Even though we did not add any explicit fixture data to our fixture, the fixture manager will truncate our tbl_project_user_role table, thereby removing all previously inserted rows before each test. We can now run our tests multiple times without incurring any database constraint errors.

When we change a user's role within a project, or remove a user from a project, we will need to remove this association. So, let's also add a method to do that. We can keep working with the same test method.

Let's alter our test and add a call to remove the association, just after we add it:

public function testUserRoleAssignment() 
{
    $project = $this->projects('project1'); 
    $user = $this->users('user1'); 
    $this->assertEquals(1,$project->associateUserToRole('owner', $user->id));
    $this->assertEquals(1,$project->removeUserFromRole('owner', $user->id));
}

Run the test again and, of course, it will fail. We need to implement this new method on the Project AR class. Add the following method at the bottom of that class:

/**
  * removes an association between the project, the user and the user's role within the project
  */ 
public function removeUserFromRole($role, $userId) {
    $sql = "DELETE FROM tbl_project_user_role 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", $userId, PDO::PARAM_INT); 
    $command->bindValue(":role", $role, PDO::PARAM_STR);
    return $command->execute();
}

This simply deletes the row from the table that houses the association between the role, user and the project. It will return the number of rows affected, which, if it successfully deleted a row, should be 1. So far, our test adds a new association and then removes it. We should run it again to make sure everything passes:

% phpunit unit/ProjectTest.php 
PHPUnit 3.4.12 by Sebastian Bergmann. 
...... 
Time: 1 second 
OK (6 tests, 15 assertions)

We now have implemented the methods for adding and removing our associations. We now need to add functionality to determine whether or not a given user is associated with a role within the project. We will also add this as a public method to our Project AR class.

So, starting with a test, add the following test method to ProjectTest.php:

public function testIsInRole()
{
    $project = $this->projects('project1'); 
    $this->assertTrue($project->isUserInRole('member'));
}

This is designed to test the implementation of the Project::isUserInRole() method. As we have not implemented this method yet, our test will certainly fail. Let's ensure it does:

% phpunit unit/ProjectTest.php 
PHPUnit 3.4.12 by Sebastian Bergmann. 
......E 
Time: 0 seconds 
There was 1 error:
 
1) ProjectTest::testIsInRole 
CException: Project does not have a method named "isUserInRole". 
... 
FAILURES! Tests: 7, Assertions: 15, Errors: 1.

To get it to pass, add the following method to the bottom of the Project AR model class:

/**
 * @return boolean whether or not the current user is in the specified role within the context of this project
 */ 
public function isUserInRole($role) 
{
    return true;
}

This should be enough to get our test to pass:

% phpunit unit/ProjectTest.php 
... 
OK (7 tests, 16 assertions)

Now we need to implement the appropriate logic to see if an association exists. Alter the method in the Project AR class to be:

public function isUserInRole($role) 
{
    $sql = "SELECT role FROM tbl_project_user_role 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->execute()==1 ? true : false;
}

This again executes the SQL directly to select from our table. It expects an input role name and uses the current application user, defined by Yii::app()->user, to make up the primary key it is searching for. Run the test again:

% phpunit unit/ProjectTest.php 
... 
Time: 1 second, Memory: 14.25Mb 
There was 1 failure:
1) ProjectTest::testIsInRole 
Failed asserting that <boolean:false> is true. 
... 
FAILURES! 
Tests: 7, Assertions: 16, Failures: 1.

Our test is failing again. The test is failing because the isUserInRole() method is using Yii::app()->user->getId() to get the current user ID, and this is returning nothing. Our test did not explicitly set the current user prior to making this call. Let's add the needed logic to properly set the current user's user ID. Alter the test method to be:

public function testIsInRole() 
{
    $user = $this->users('user1'); 
    Yii::app()->user->setId($user->id); 
    $project = $this->projects('project1'); 
    $this->assertTrue($project->isUserInRole('member'));
}

This sets the current user ID to that of user1 from our users fixture data. Now run the test again:

% phpunit unit/ProjectTest.php 
... 
Time: 1 second, Memory: 14.25Mb 
There was 1 failure:
1) ProjectTest::testIsInRole 
Failed asserting that <boolean:false> is true. 
... 
FAILURES! 
Tests: 7, Assertions: 16, Failures: 1.

Our test is still failing, but now it is failing because the row does not exist in the table, the user ID of user1 is not associated with the owner role for this project. So, let's create that association before we call the isUserInRole() method.

We could use the other methods we added and tested earlier to create and remove these associations in order to establish the relationship. However, in an attempt to keep this test as isolated as possible from other tests or Project AR methods, we'll lean on fixture data to provide the initial conditions.

When we first added the fixture file tests/fixtures/tbl_project_user_role. php, we had it simply return an empty array. Let's change that to have it populate a row with a project ID of 2, a user ID of 2, and a role name of member:

return array( 
    'row1'=>array(
        'project_id' => 2, 
        'user_id' => 2, 
        'role' => 'member',
    ),
);

As our previous tests for the adding and removing of associations are using user1 and project1 fixture data, we've played it safe to avoid any conflicts by using different IDs to seed this data.

Now we'll use this fixture data to set our application user ID as well as to create the Project AR class. Alter the test method to be:

public function testIsInRole() 
{
    $row1 = $this->projUserRole['row1']; 
    Yii::app()->user->setId($row1['user_id']); 
    $project=Project::model()->findByPk($row1['project_id']);
    $this->assertTrue($project->isUserInRole('member'));
}

Here we are using the fixture data defined at the top of the class, called projUserRole, to retrieve our seeded row data. We then use this data to set the user ID, and create the Project AR instance by calling Project::model()->findByPk. We then test to ensure the user has, indeed, been associated with the member role. Now if we run our test:

% phpunit unit/ProjectTest.php 
... 
OK (7 tests, 16 assertions)

Our test is passing once again.

We have written and tested the methods to add and remove our role associations within a project, and the method to determine whether or not a given user is associated with a project role. We are going to write one final test. We are going to write a test for our end-to-end implementation of how we plan to add this extra project dimension to Yii's RBAC structure. We talked about achieving this by adding a business rule to the Yii RBAC auth assignment whenever we associate a user to a role. Let's write one final method in to test this approach.

Open back up the ProjectTest.php unit test file, and add the following test method:

public function testUserAccessBasedOnProjectRole() 
{
    $row1 = $this->projUserRole['row1'];
    Yii::app()->user->setId($row1['user_id']);
    $project=Project::model()->findByPk($row1['project_id']); 
    $auth = Yii::app()->authManager;
    $bizRule='return isset($params["project"]) && $params["project"]->isUserInRole("member");';
    $auth->assign('member',$row1['user_id'], $bizRule); 
    $params=array('project'=>$project); 
    $this->assertTrue(Yii::app()->user->checkAccess('updateIssue', $params));
    $this->assertTrue(Yii::app()->user->checkAccess('readIssue',$ params));
    $this->assertFalse(Yii::app()->user->checkAccess('updateProje ct',$params));
}

This final test method uses other existing, and already tested, API methods to achieve the test, so there is no need to go through our normal TDD steps. In some ways it could be argued that this is more of a functional test than a unit test, but we think it still belongs in this unit test class.

We will take the same approach (as we did with the previous test) to setup our user ID, and establish the project AR instance by using the data from the tbl_project_ user_role.php fixture file. We then create an instance of the auth manager class that we use to establish the assignment of the user to the role owner. However, before we make that assignment, we create the business rule. The business rule uses the $params array by first checking the existence of a project element in the array, and then calls the isUserInRole() method on the Project AR class, which it assumes is the value of that array element. We explicitly pass in the name, owner, to this method, as that is the role we are going to be assigning. Finally, we make the call to the Yii RBAC related method Yii::app()->user->checkAccess() to see if the current user, who has now been assigned to the role owner in our RBAC auth hierarchy as well as is associated with this role within the project.

We are checking whether or not the user has the permission to update an issue, which we know anyone in the member role should have. We expect this to return true. We are also making a couple of other assertions to test (and demonstrate) the permission inheritance. We expect a user in the member role to inherit permissions from reader. So we also test that the user has access to the readIssue permission, which we know is a child of the reader role in our auth hierarchy. Finally, we should expect to be denied access to operations exclusive to the owner role. So we test to ensure false is returned when we check access to the updateProject operation.

Running the tests again:

% phpunit unit/ProjectTest.php 
... 
OK (8 tests, 19 assertions)

All the project tests are passing. It seems as if this approach will do the trick.

As we are explicitly using the code:

$auth->assign('member',$row1['user_id'], $bizRule);

to insert a row in the AuthAssignment table, we will get a database integrity violation if we attempt to run this test again. Basically, it will be try to re-insert the same row, and this will violate a data integrity constraint we have defined on this table. To avoid this, we need to allow the fixture manager to also manage this table. We have seen this before. Simply add a new file to the fixtures folder protected/ tests/fixtures/AuthAssignment.php, and have it return an empty array. Then, alter the fixtures array defined at the top of the ProjectTest.php file to include this in the fixtures definition:

public $fixtures=array( 
    'projects'=>'Project',
    'users'=>'User', 
    'projUsrAssign'=>':tbl_project_user_assignment', 
    'projUserRole'=>':tbl_project_user_role', 
    'authAssign'=>':AuthAssignment',
);

Now our AuthAssignment table will be reset to a consistent state before each test is run.

Before we leave this test, let's add a little more to ensure that if we pass in a project to which the user is not assigned, they have no access. As we explicitly set up the association to be with project id #2, let's just check the user's access using project id #1. Add the following at the end of the testUserAccessBasedOnProjectRole() method:

//now ensure the user does not have any access to a project they are not associated with
$project=Project::model()->findByPk(1); 
$params=array('project'=>$project); 
$this->assertFalse(Yii::app()->user->checkAccess('updateIssue', $params)); 
$this->assertFalse(Yii::app()->user->checkAccess('readIssue', $params)); 
$this->assertFalse(Yii::app()->user->checkAccess('updateProject', $params));

Here we are creating a new project instance based on project_id = 1. We know the user is not associated with this project at all, so all of the checkAccess() calls should return false.

Adding Users To Projects

In the previous iteration, we added the ability to create new users of the application. However, we do not yet have a way to assign users to specific projects, and further, assign them to roles within these projects. Now that we have our RBAC approach in place, we need to build out this new functionality.

The implementation of this needed functionality involves several coding changes. However, we have provided similar examples of the types of changes needed, and have covered all of the related concepts when implementing functionality from previous iterations. Consequently, we will move pretty quickly through this, and pause only briefly to highlight just a few things we have not yet seen. At this point, the reader should be able to make all of these changes without much help, and is encouraged to do so as a hands-on exercise. To further encourage this exercise, we'll first list everything we are going to do fulfill this new feature requirement. You can then close the book and try some of these out yourself before looking further down at our implementation.

To achieve this goal we will perform the following:

  • Using a test-first approach, 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. We will use this to populate a roles selection drop-down field in the form for adding a new user to a project.
  • Using a test-first approach, add a new public method called associateUserToProject($user) to the Project model class to associate a user to a project. This can insert directly into the tbl_project_use_ assignment table to make an association between the user and the project.
  • Using a test-first approach, add a new public method called isUserInProject($user) to the Project model class to determine if a user is already associated with a project. We will use this in our validation rules upon form submission so that we don't attempt to add a duplicate user to 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: $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 through a custom verify() class method. This verify() method should:
    • Attempt to create a new User AR class instance by finding a user by matching the input username.
    • If the attempt was successful, it should continue to associate the user to a project using the new method, associateUserToProject($user), added previously as well as associate the user to the role in the RBAC approach discussed 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, which is a dropdown 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 handling the post back when the form is submitted.

Again, we encourage the reader to attempt these changes on their own first. We list our code changes in the following sections.

Altering the Project model class

To the Project class, we added three new public methods, one of them static so it can be called without the need for a specific instance:

/** 
  * 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'); 
}
 
/** 
  * Makes an association between a user and a the project 
  */
public function associateUserToProject($user) 
{
    $sql = "INSERT INTO tbl_project_user_assignment (project_id, user_id) VALUES (:projectId, :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->execute();
} 
 
/**
  * 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->execute()==1 ? true : false;
}

There is nothing special to further describe in the preceding code. As these were all public methods on the Project model class, we ended up with the following two test methods within the ProjectTest unit test class:

public function testGetUserRoleOptions() 
{
    $options = Project::getUserRoleOptions();
    $this->assertEquals(count($options),3);
    $this->assertTrue(isset($options['reader']));
    $this->assertTrue(isset($options['member']));
    $this->assertTrue(isset($options['owner']));
}
 
public function testUserProjectAssignment() {
    //since our fixture data already has the two users 
    //assigned to project 1, we'll assign user 1 to project 2
    $this->projects('project2')->associateUserToProject($this- >users('user1'));
    $this->assertTrue($this->projects('project1')->isUserInProject($this->users('user1')));
}

Adding the new form model class

Just as was used in the approach for the login form, we are going to create a new form model class as a central place to house our form input parameters and to centralize the validation. This is a fairly simple class that extends from the Yii class CFormModel and has attributes that map to our form input fields, as well as one to hold the valid project context. We need the project context to be able to add users to projects. The entire class is listed as follows:

<?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 'Adduser' 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;
 
    /** 
      * 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 password are required 
            array('username, role', 'required'),
            // password needs to be authenticated 
            //array('username', 'verify'), 
            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->project->associateUserToProject($user); 
                $this->project->associateUserToRole($this->role, $user->id);
                $auth = Yii::app()->authManager;
                $bizRule='return isset($params["project"]) && $params["project"]->isUserInRole("'.$this->role.'");';
                $auth->assign($this->role,$user->id, $bizRule);
            }
        }
    }
}

Adding the new action method to the project controller

We need a controller action to handle the initial request to display the form for adding a new user to a project. We placed this in the ProjectController class and named it actionAdduser(). The code for this is as follows:

public function actionAdduser() 
{
    $form=new ProjectUserForm; 
    $project = $this->loadModel();
    // collect user input data
    if(isset($_POST['ProjectUserForm'])) {
        $form->attributes=$_POST['ProjectUserForm']; 
        $form->project = $project; // validate user input and set a sucessfull flassh message if valid
        if($form->validate()) 
        {
            Yii::app()->user->setFlash('success',$form->username . " has been added to the project." );
            $form=new ProjectUserForm;
        }
    }
    // display the add user form 
    $users = User::model()->findAll(); 
    $usernames=array(); 
    foreach($users as $user) 
    {
        $usernames[]=$user->username;
    }
    $form->project = $project; 
    $this->render('adduser',array('model'=>$form, 'usernames'=>$usernames)); 
}

This is all pretty familiar to us at this point. It handles both the initial GET request to display the form as well as the POST request after the form is submitted. It follows very much the same approach as our actionLogin() method in our site controller. The preceding highlighted code is, however, something we have not seen before. If the submitted form request is successful, it sets what is called a flash message. A flash message is a temporary message stored briefly in the session. It is only available in the current and the next requests. Here we are using the setFlash() method of our CWebUser application user component to store a temporary message that the request was successful. When we talk about the view next, we will see how to access this message, and display it to the user.

Also, in the previous code, we created an array of available usernames from the system. We will use this array to populate the data of one of Yii's UI widgets, CAutoComplete, which we will use for the username input form element. As its name suggests, as we type in the input form field, it will provide choice suggestions based on the elements in this array.

One other change we had to make to the ProjectController class, was to add in this new action method to the basic access rules list so that a logged in user is allowed to access this action:

public function accessRules() 
{
    return array( 
        array('allow', // allow all users to perform 'index' and 'view' actions 
        'actions'=>array('index','view', 'adduser'),
        'users'=>array('@'),
    ),
    ...

Adding the new view file to display the form

Our new action method is calling ->render('adduser') to render a view file, so we need to get that created. A full listing of our implementation for protected/views/ project/adduser.php is as follows:

<?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('CAutoComplete', array(
                 'model'=>$model, 
                 'attribute'=>'username', 
                 'data'=>$usernames, 
                 'multiple'=>false, 
                 'htmlOptions'=>array('size'=>25),
             )); ?>
             <?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>

Most of this we have seen before. We are defining active labels and active form elements that tie directly to our ProjectUserForm form model class. We populate our dropdown using the static method we implemented earlier on the project model class. We also added a simple link to the menu op to take us back to the project details page.

The highlighted code above is new to us. This is an example of using the flash message that we introduced and used in the actionAdduser() method. We access the message we set using setFlash() by asking the same user component if it has a flash message, using hasFlash('succcess'). We feed the hasFlash() method the exact name we gave it when we set the message. This is a nice way to present the user with some simple feedback about their previous request.

One other small change we made as to add a simple link from the project details page so we could access this form form the application. The following line was added to the project show.php view file's list of link options:

[<?php echo CHtml::link('Add User To Project',array('adduser','id'=>$model->projectId)); ?>]

This gives us access to the new form.

Putting it all together

With all of these changes in place, we can navigate to our new form by viewing one of the project details pages. For example, viewing project id #1 through the URL: http://localhost/trackstar/index.php?r=project/view&id=1. In the right column menu of operations is a hyperlink Add User To Project and clicking on that link should display the following page:

You can use the forms we have previously built to create new projects and users to ensure you have a few added to the application. Then you can play around with adding users to projects. As you type in the Username field, you will see suggestions for auto-completion. If you attempt to add a user that is not in the user database table, you should see an error telling you so. If you attempt to enter a user that has already been added to the project, you will see an error message. On successful additions, you will see a short flash message indicating success.

Checking authorization level

The last thing we need to do in this iteration is to add the authorization checks for the different functionality that we have implemented. Earlier in this chapter we outlined and then implemented the RBAC authorization hierarchy for the different roles we have. Everything is in place to allow or deny access to functionality based on the permissions that have been granted to users within projects, with one exception. We have not yet implemented the necessary access checking when attempting to request functionality. The application is still using the simple access filter that is defined on each of our project, issue and user controllers. We'll do this for one of our permissions and then leave the remaining implementation as an exercise for the reader.

We can notice from looking back at our authorization hierarchy that only project owners should be able to add new users to a project. So, let's start with that. What we will do is not even display the link on the project details page unless the current user is in the owner role for that project (you might want to make sure you have added at least one owner and one member or reader to a project so you can test it when complete). Open up the protected/views/project/view.php view file where we placed the link on the menu items for adding a new user. Remove that array element from the menu array items, and then push it on the end of the array only if the checkAccess() method returns true. The following code shows how the menu items should be defined:

$this->menu=array( 
    array('label'=>'List Project', 'url'=>array('index')), 
    array('label'=>'Create Project', 'url'=>array('create')), 
    array('label'=>'Update Project', 'url'=>array('update', 'id'=>$model->id)), 
    array('label'=>'Delete Project', 'url'=>'#', 
         'linkOptions'=>array(
            'submit'=>array('delete','id'=>$model->id),
            'confirm'=>'Are you sure you want to delete this item?'
        )
    ),
    array('label'=>'Manage Project', 'url'=>array('admin')),
    array('label'=>'Create Issue', 'url'=>array('issue/create', 'pid'=>$model->id)),
);
if(Yii::app()->user->checkAccess('createUser',array('project'=>$model))) 
{
    $this->menu[] = array(
            'label'=>'Add User To Project', 
            'url'=>array('adduser', 'id'=>$model->id)
    ); 
}

This implements the same approach we had discussed earlier in the chapter. We call checkAccess() on the current user, and send in the name of the permission we want to check. Also, as our roles are within the context of projects, we send in the project model instance as an array input. This will allow the business rule to execute what has been defined in the authorization assignment. Now if we log in as a project owner for a particular project and navigate to that project details page, we'll see the menu option for adding a new user to the project. Conversely, if you log in in as a user in the member or reader role of that same project, and again navigate to the details page, this link will not display.

This, of course, will not prevent a savvy user from gaining access to this functionality by navigating using the URL directly. For example, even while logged in to the application as a user in the reader role for, say, project id #2, if I navigate directly to the URL: http://hostname/tasctrak/index.php?r=project/adduser&id=2 I can still access the form.

To prevent this, we need to add our access check directly to the action method itself. So, in the actionAdduser() method in the project controller class, we can add the check:

public function actionAdduser() 
{
    $project = $this->loadModel();
    if(!Yii::app()->user->checkAccess('createUser', array('project'=>$project)))
    {
        throw new CHttpException(403,'You are not authorized to per-form this action.');
    }
    $form=new ProjectUserForm;
    // collect user input data 
    ...

Now when we attempt to access this URL directly, we will be denied access unless we in the project owner role for the project.

We won't go through implementing the access checks for all of the other functionality. Each would be implemented in a similar manner.

Summary

We have covered a lot in this iteration. First we were introduced to the basic access control filter that Yii provides as one method to allow and deny access to specific controller action methods. We used this approach to ensure that users be logged into that application before gaining access to any of the main functionality. We then took a detailed walk through Yii's RBAC model which allows for much more sophisticated approach to access control. We built an entire user authorization hierarchy based on application roles. In the process, we were introduced to writing console applications in Yii, and to some of the benefits of this wonderful feature. We then built in new functionality to allow the addition of users to projects and being able to assign them to appropriate roles within those projects. Finally, we discovered how to implement the needed access checks throughout the application to utilize the RBAC hierarchy to appropriately grant/deny access to feature functionality.

评论 X

      友荐云推荐
      Copyright 2011-2014. YiiBook.com