《Yii快速应用程序开发(高手)》

Chapter 4. Level Up! Permission Levels

In this mission, we will implement a lending function and a fine grained access control. In our experience, most projects require the ability to define permissions at a very precise level. A good example of this is providing users with the ability to edit their own account information, but not the account information of other users. In this project, we will use Yii and available extensions to construct a custom permissions system for our comic book application.

Mission Briefing

We will add a library management page for you to manage the books you share and a lend/borrow page for your friends to see books that they can borrow and request. Then, we will replace the default Yii user access with a more extensive user management system that includes roles and access levels.

Why Is It Awesome?

Almost any web application you make is going to have users with different levels of access. This project will demonstrate a method for adding users and access control to any site you build with Yii. We will also touch on some website security issues here, but encourage you to study this topic well and enhance your knowledge of security with every site you build.

Your Hotshot Objectives

Here is an overview of the project steps:

  • Adding Admin Function – Library Management
  • Adding User Functions – Library
  • Defining Roles and Access
  • Adding the RBAC Extension
  • Adding Roles to User Management
  • Fine-tuning Permissions
  • Making History

Mission Checklist

This project assumes that you have a web development environment prepared. If you do not have one, the tasks in Project 1, Develop a Comic Book Database, will guide you through setting one up. In order to work this project, you will need to set up the project files that have been provided with the book. Refer to the Preface of the book for instructions on downloading these files. The files for this project include a Yii project directory with a database schema. To prepare for the project follow these steps, replacing the username lomeara with your own username:

  1. Copy the project files into your working directory.
cpr ~/Downloads/project_files/Chapter\ 4/project_files ~/projects/ch4
  1. Make the directories that Yii uses web writeable.
cd ~/projects/ch4/
sudo chown -R lomeara:www-data protected/runtime assets protected/models protected/controllers protected/views
  1. Create a link in the webroot directory to the copied directory.
cd /opt/lampp/htdocs
sudo ln -s ~/projects/ch4 cbdb
  1. Import the project into NetBeans (remember to set the project URL to http://localhost/cbdb) and configure for Yii development with PHPUnit.
  2. Create a database named cbdb and load the database schema (~/projects/ch4/protected/data/schema.sql) into it.
  3. If you are not using the XAMPP stack or if your access to MySQL is password protected, you should review and update the Yii configuration file (in NetBeans, it is ch4 | Source Files | protected | config | main.php).

The admin login is admin/test.

Adding Admin Function – Library Management

Because we like to share books from our collection with our friends, we will add a lending function to our site. When a friend logs in, he/she will be able to see which books are available for borrowing, which he/she is currently borrowing, and which he/she has borrowed in the past.

Engage Thrusters

  1. First, we will expand our comic book management interface with some new lending information. We will note which books we are willing to lend and while we are at it, let's make a field to track who is currently borrowing a book. Open an SQL command window for the cbdb database and run the following commands:
ALTER TABLE book ADD borrower_id int(10) unsigned default null;
ALTER TABLE book ADD lendable boolean default true;
ALTER TABLE book ADD FOREIGN KEY ('borrower_id') REFERENCES user(id);
  1. Now we must expand the book model to include the new fields (ch4 | Source Files | protected | models | Book.php).

i. At the top of the model, add the new fields to the comments. This is not required, but it is good practice.

* @property string $borrower_id
* @property integer $lendable

ii. Add a relation named borrower to connect the user who has borrowed the book, if borrower_id is set.

* @property User $borrower

iii. Add lendable to the numeric field check in the rules function.

array('signed, bagged, lendable', 'numerical', 'integerOnly'=>true),

iv. Add a check to make sure borrower_id has a maximum length of 10 in the rules() function.

array('type_id, value, price, grade_id, issue_number', 'length', 'max'=>10),

The previous code then becomes:

array('type_id, value, price, grade_id, issue_number, borrowed_id', 'length', 'max'=>10),
      array('type_id, value, price, grade_id, borrower_id', 'length', 'max'=>10),

v. Add lendable to the searchable list.

array('id, title, type_id, publication_date, value, price, notes, signed, grade_id, 
    bagged, issue_number, lendable', 'safe', 'on'=>'search'),

vi. Add entries in the attributeLabels() function.

'borrower_id' => 'Borrower',
'lendable' => 'Lendable',

vii. Add lendable to the compare criteria in the search function.

$criteria->compare('lendable',$this->lendable,true);

viii. Finally, add an entry named borrower for the lent field in the relations function.

'borrower' => array(self::BELONGS_TO, 'User', 'borrower_id'),
  1. Now we can expand the book editing form to include the new fields (ch4 | Source Files | protected | views | book | _form.php). Add a checkbox for the lendable field.
<div class="row">
    <?php echo $form->labelEx($model,'lendable'); ?>
    <?php echo $form->checkbox($model, 'lendable'); ?>
    <?php echo $form->error($model,'lendable'); ?>
</div>

Open a book form by either editing an existing book or creating a new one. Notice that the lendable field is, by default, checked. This is because, when we created the field in MySQL, we specified that by default the value will be true. We are very generous with lending our books.

  1. We will add the borrower to the form as an Ajax autocomplete field providing a list of users. To support this, we must expand the Book model (ch4 | Source Files | protected | models | Book.php) to include some transition fields. Add the following variables at the top of the Book class:
public $borrower_fullname = '';
public $borrower_fname;
public $borrower_lname;

Also add borrower_fname and borrower_lname to the searchable list in the rules function.

array('id, title, type_id, publication_date, value, price, notes, signed, grade_id, 
    bagged, issue_number, lendable, borrower_fname, borrower_lname', 
        'safe', 'on'=>'search'),
  1. Now, we will add an Ajax function, named aclist, to the user controller (ch4 | Source Files | protected | controllers | UserController.php) that will take the field input as a filter and return a list of users.

i. Add the new action to the admin access list in the User controller.

'actions'=>array('index', 'view', 'create', 'update', 'delete', 'aclist'),

ii. Add the Aclist action to the User controller.

public function actionAclist($term)
{
    $results=array();
    $model = User::model();
    $criteria = new CDbCriteria();
    $criteria->with = array('person');
    $names = preg_split('/\W/', $_GET['term'], 2);
 
    if (count($names) == 1) { 
        $criteria->addSearchCondition( 'person.fname', $names[0], true, 'OR');
        $criteria->addSearchCondition( 'person.lname', $names[0], true, 'OR');
    } else {
        $criteria->compare('person.fname', $names[0], true);
        $criteria->compare('person.lname', $names[1], true);
    } 
    foreach($model->findAll($criteria) as $m)
    { 
        $results[] = array(
            'id' => $m->{'id'},
            'label' => $m->person->{'fname'} . ' ' . $m->person->{'lname'}, 
            'value' => $m->person->{'fname'} . ' ' . $m->person->{'lname'}, 
        ); 
    } 
    echo CJSON::encode($results);
}
  1. Create a function in the Book controller that joins the first and last name of a borrower into a single full name field.
public function set_fullname($model) {
    if ($model->borrower_id != null) {
        $model->borrower_fullname =
            ($model->borrower->person->fname ? $model->borrower->person->fname . ' ' : '') .
            ($model->borrower->person->lname ? $model->borrower->person->lname : '') ;
    }
}
  1. Change the update action in the Book controller to use the set_fullname function, by first loading the model, then setting the full name value, and then calling the parent update function.
public function actionUpdate($id)
{
    $model=$this->loadModel($id);
    $this->set_fullname($model);
    $this->update($model);
}
  1. In the Book Edit form (ch4 | Source Files | protected | views | book | _form.php), we add an auto-complete field for borrower. This field uses a Jui Zii extension, which uses jQuery to make the Ajax calls and process the responses.
<div class="row">
<?php
    echo CHtml::activeHiddenField($model,'borrower_id');
    echo $form->labelEx($model,'borrower');
    $this->widget('zii.widgets.jui.CJuiAutoComplete', array(
        'model' => $model,
        'attribute' => 'borrower_fullname',
        'sourceUrl' => array('user/aclist'),
        'name' => 'borrower_fullname',
        'options' => array (
            'minLength' => '3',
            'select'=> new CJavaScriptExpression('function( event, ui ) {
                $("#\'.CHtml::activeId($model,\'borrower_id\').\'")
                .val(ui.item.id);
                return true;
            }'
        ),
        'htmlOptions' => array (
            'size' => 32,
            'maxlength' => 32,
            'value' => $model->borrower_fullname,
        ),
    ));
?>
</div>

Now, add a borrower, edit a book, and type three letters of the first or last name of a user. When you stop typing, you should see a drop-down list, containing the first name and last name entries for all of the matching users. If you are using the schema for the chapter, try rie. The results should be Best Friend and Another Friend.

  1. Most of the time, a borrower will not be set when a book is created, so we must handle this case. (You can test it out by creating a book before taking this step.) Add an entry to the rules array in the Book model (ch4 | Source Files | protected | models | Book.php) to set the value of borrower_id to null, if no value is supplied.
array('borrower_id', 'default', 'setOnEmpty' => true),
  1. To see the new fields on the Book Index page, update the Book Index action in the Book controller (ch4 | Source Files | protected | controllers | BookController.php) to create a CDbCriteria object with the borrower.person relation and include that object in the creation of the CActiveDataProvider object.
$criteria=new CDbCriteria;
$criteria->with = array('borrower.person');
    $dataProvider=new CActiveDataProvider('Book', array(
    'criteria' => $criteria,
));
  1. Add the following fields to ch4 | Source Files | protected | views | book | _view.php:
<b><?php echo CHtml::encode($data->getAttributeLabel('lendable')); ?>:</b>
<?php echo CHtml::encode($data->lendable); ?>
<br />
 
<?php
    echo "<b>" .
    CHtml::encode($data->getAttributeLabel('borrower')) . ":</b>";
    BookController::set_fullname($data);
    echo CHtml::encode($data->borrower_fullname);
?>
<br />
  1. Then, update the Book View action to first load the model, then set the fullname, and pass it to the view as follows:
public function actionView($id)
{
    $model = $this->loadModel($id);
    $this->set_fullname($model);
    $this->render('view',array(
        'model'=>$model,
    ));
}
  1. Add the fields to the attributes array in the book view (ch4 | Source Files | protected | views | book | view.php), and the individual book page will display the new fields.
'lendable',
array(
    'name' => 'borrower',
    'value' => $model->borrower_fullname,
),
  1. To display the new fields in the admin page, edit the Book model (ch4 | Source Files | protected | models | Book.php), and add the following to the search function to search on the lendable field with the values yes or no:
$criteria->compare('lendable',($this->lendable=="yes" ? 1: 
    ($this->lendable=="no" ? 0 : "")),true);

Add these to include the related borrower and person information and to search on the borrower's first and last names.

$criteria->compare('person.fname', $this->borrower_fname, true);
$criteria->compare('person.lname', $this->borrower_lname, true);
$criteria->with = array('borrower.person');
  1. Also, add a CSort object to the search function as follows:
$sort = new CSort;
$sort->attributes = array(
    'borrower_fname' => array(
        'asc' => 'person.fname',
        'desc' => 'person.fname DESC',
    ),
    'borrower_lname' => array(
        'asc' => 'person.lname',
        'desc' => 'person.lname DESC',
    ),
    '*',
);
  1. Pass the sort variable to the CActiveDataProvider object as follows:
return new CActiveDataProvider($this, array(
    'criteria'=>$criteria,
    'sort'=>$sort,
));
  1. Finally, add the columns to the admin view (ch4 | Source Files | protected | views | book | admin.php) list.
array(
    'name' => 'lendable',
    'header' => 'Lendable',
    'value' => '(($data->lendable == 1) ? "yes" : "no")', 
),
array(
    'name' => 'borrower_fname',
    'header' => 'Borrower First Name',
    'value' => '(($data->borrower != null) ? $data->borrower->person->fname . \' \' : \'\')',
),
array(
    'name' => 'borrower_lname',
    'header' => 'Borrower Last Name',
    'value' => '(($data->borrower != null) ? $data->borrower->person->lname . \' \' : \'\')',
),

The final Book admin screen will look like the following screenshot:

Objective Complete - Mini Debriefing

To add the library management piece, we started by adding fields to the book table in the database. We updated the model to include the new fields. Then piece by piece, we added support for the new field in each of the book views. One area that we did not touch on was the Advanced Search form. If you would like, you can update this form to include the fields that you find useful for searching, but do not necessarily need for quick searching. To do this, edit ch4 | Source Files | protected | views | book | _search.php.

Adding User Functions – Library

Because we like to share books from our collection with our friends, we will add a lending function to our site. When a friend logs in, he/she will be able to see which books are available for borrowing, which he/she is currently borrowing, and which he/she has borrowed in the past.

Prepare for Lift Off

To demonstrate the effect of lendable/not lendable in the library, edit a few books and turn off the Lendable field.

Engage Thrusters

  1. To access the new feature, we need to add a Library option to the menu, in the file ch4 | Source Files | protected | views | layouts | main.php.
array('label'=>'Library', 'url'=>array('/library/index')),

Log in to the site and you will see the updated menu, but clicking on this link will cause a page not found (404) error. We must create a new controller and view to support it.

  1. Right-click on the controllers folder (ch4 | Source Files | protected | controllers) and select New | PHP File.
  1. Enter LibraryController in the File Name field and click on Finish.
  1. Paste the following contents into the file:
<?php
 
class LibraryController extends Controller
{
 
  /**
   * @return array action filters
   */
  public function filters()
  {
    return array(
      'accessControl', // perform access control for CRUD operations
    );
  }
 
  /**
   * 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' action
        'actions'=>array('index'),
        'users'=>array('*'),
      ),
      array('deny',  // deny all users
        'users'=>array('*'),
      ),
    );
  }
 
  /**
   * Display library
   */
    public function actionIndex()
    {
        $criteria=new CDbCriteria;
        $criteria->compare('lendable', 1);
        $dataProvider=new CActiveDataProvider('Book', array(
            'criteria' => $criteria,
        ));
        $this->render('index',array(
            'dataProvider'=>$dataProvider,
        ));
    }
}

We have chosen to explicitly state that access rules apply to all users. You can leave the user line out, if you prefer, as it is the default value.

We have added a controller with access control enabled, default layout, and one action, that is Index, which returns all the books that are lendable and not currently lent. Now if you click on Library in the menu, the error will look even worse!

  1. We will fix the error by creating a view to present the results of the Library controller Index action. Begin by creating a new view folder. In NetBeans, right-click on views under ch4 | Source Files | protected and select New | Folder.
  2. Enter library in the Folder Name, and click on Finish.
  3. Right-click on the new library folder and select New | PHP File. Name the file index and click on Finish.
  4. Replace the contents of the new index.php file with the following grid view
<?php $this->widget('zii.widgets.grid.CGridView', array(
    'id'=>'book-grid',
    'dataProvider'=>$dataProvider,
    'columns'=>array(
        'title',
        'issue_number',
        array(
            'name' => 'Type', 
            'header' => 'Type', 
            'value' => '$data->type->name',
        ),
        array(
            'name' => 'Publisher', 
            'header' => 'Publisher', 
            'value' => '(($data->publisher!=null) ? $data->publisher->name : \'\')',
        ),
        array(
            'name' => 'Authors', 
            'header' => 'Authors', 
            'value' => array($dataProvider->model, 'author_list'),
        ),
        'publication_date',
        array (
            'class'=>'CButtonColumn',
            'template'=>'{request}',
            'buttons' => array(
            'request' => array(
                'label' => 'Request', 
                'imageUrl' => Yii::app()->baseUrl . '/images/request_lozenge.png',
                'url' => 'Yii::app()->createUrl("library/request", array("id"=>$data->id))',
            ),
        ),
    ),
),
)); ?>
  1. In the library index, the Authors column takes advantage of a CGridView feature that allows you to specify an object and a function to supply the value for the column. In this case, we call the author_list function in the Book model. Open up the Book model and add that function.
public function author_list($data, $row) {
    $list = array();
    $count = 1;
    foreach ($data->authors as $a) {
        $list[] = $a->fname . ' ' . $a->lname;
        $count++;
    }
    return implode(",", $list);
}

Next, we will add a Book model function to display the book's lending status in the status column.

  1. It will be nice to see the lending status of the books, whether we have them or someone else does. Let's add a status column to the library index view.
'publication_date',
array(
    'name' => 'Status',
    'header' => 'Status',
    'value' => array($dataProvider->model, 'get_status'),
),
array (
    'class'=>'CButtonColumn',
  1. Also, add a supporting function to the Book model.
public function get_status($data, $row) {
    $status ="";
    if ($data->borrower_id != null) {
        $status = "Checked Out";
    }
    if ($data->borrower_id == Yii::app()->user->getId()) {
        $status = "You Have It";
    }
    if ($status == null) {
        $status = "Available";
    }
    return $status;
}
  1. Also in the Library index view, we added a custom link to request a book. We must add the action to the Library controller (ch4 | Source Files | protected | controllers | LibraryController.php) to process the request. But first, we will need a table to record the request.
CREATE TABLE 'request' (
    'book_id' int(10) unsigned NOT NULL,
    'requester_id' int(10) unsigned NOT NULL,
    PRIMARY KEY ('book_id','requester_id'),
    KEY 'requester_id' ('requester_id'),
    KEY 'book_id' ('book_id'),
    CONSTRAINT  FOREIGN KEY ('book_id') REFERENCES 'book' ('id'),
    CONSTRAINT FOREIGN KEY ('requester_id') REFERENCES 'user' ('id')
) ENGINE=InnoDB DEFAULT
  1. Use Gii to generate a model from the request table.
  2. Add the new relation to the Book model (ch4 | Source Files | protected | models | Book.php).
'requesters' => array(self::MANY_MANY, 'User',
    'request(requester_id, book_id)',
        'index'=>'id'),
'requests' => array(self::HAS_MANY, 'Request',
    'book_id', 'index' => 'requester_id'),
  1. Create the request action in the Library controller (ch4 | Source Files | protected | controllers | LibraryController.php).
public function actionRequest($id)
{
    $request = new Request();
    $request->book_id = $id;
    $request->requester_id = Yii::app()->user->getId();
    $request->save();
    Yii::app()->user->setFlash ('success', "Your book request has been submitted.");
    $this->redirect(array('index'));
}
  1. Add the access authorization to the Library controller access rules.
'actions'=>array('index', 'request'),
  1. Add a flash display at the top of the Library index view to display success when a request is recorded. A flash message keeps the message in session through one or more of the user's requests.
<?php if(Yii::app()->user->hasFlash('success')) { ?>
<div class="flash-success">
    <?php echo Yii::app()->user->getFlash('success'); ?>
</div>
<?php } ?>
  1. Add the parameter visible to the CButtonColumn in the Library index view to display the Request link if no request has been made. Once again, we will use a function defined on our model to get our result.
'request' => array(
    'label' => 'Request',
    'imageUrl' => Yii::app()->baseUrl . '/images/request_lozenge.png',
    'url' => 'Yii::app()->createUrl("library/request", array("id"=>$data->id))',
    'visible' => array($dataProvider->model, 'requested'),
),
  1. Add the function requested to the Book model to support the change we just made to the view. The function will return true or false to toggle the Request link in the library grid.
public function requested($row, $data) {
    $me = Yii::app()->user->getId();
 
    foreach ($data->requesters as $r) {
        if ($r->id == $me) {
            return false;
        }
    }
    if ($data->borrower_id==$me) {
        return false;
    }
    return true;
}

The finished Library view looks like the following screenshot:

  1. Now we will circle back to the book list view to display the requests and add the functions to process them. To display the list of requests for each book in the index view, add this section to the bottom of ch4 | Source Files | protected | views | book | _view.php.
<?php
if ($data->requesters) {
    echo "<strong>Requests</strong>\n";
    echo "<ul>\n";
    foreach ($data->requesters as $r) {
        echo "<li>" . CHtml::encode($r->person->fname .
            ' ' . $r->person->lname) . 
            "&nbsp;" . CHtml::link("Lend",
            array("library/lend", "book_id" => $data->id,       
            "user_id" => $r->id)) .
            "</li>";
    }
    echo "</ul>\n";
}
?>
  1. To add a link next to a borrower to process a book return, change the entry for borrower in the book view to include a return link.
<?php
echo "<b>" .
    CHtml::encode($data->getAttributeLabel('borrower')) .
    ":</b>";
BookController::set_fullname($data);
    echo CHtml::encode($data->borrower_fullname) . "&nbsp;" . 
    CHtml::link("Return", array("library/return", "book_id" => $data->id, 
        "user_id" => $data->borrower_id));
?>
<br />
  1. Also, update the Library controller with this function to process a request.
public function actionLend($book_id, $user_id)
{
    $model=Book::model()->findByPk($book_id);
    if($model===null)
        throw new CHttpException(404,'The requested book does not exist.');
    $request = Request::model()->find(
        'book_id=:book_id AND requester_id=:user_id', array(
            ':book_id' => $book_id,
            ':user_id' => $user_id,
        ));
    if($request===null)
        throw new CHttpException(404,'The request does not exist.');
 
    $request->delete();
    $model->borrower_id = $user_id;
    $model->save();
    $this->redirect(array('book/index'));
}
  1. The twin function to lend is return:
public function actionReturn($book_id,$user_id)
{
    $model=Book::model()->findByPk($book_id);
    if($model===null)
        throw new CHttpException(404,'The requested book does not exist.');
    $model->borrower_id = null;
    $model->save();
    $this->redirect(array('book/index'));
}
  1. Add the new action to the access rules. For the moment, it is ok to permit it for all users. We will adjust the permissions in the next task.
array('allow',  // allow all users to perform 'index' action
    'actions'=>array('index', 'request', 'lend', return),
    'users'=>array('*'),
),
  1. The resulting Book index page will now include a list of requests, if any requests for the book are pending.

Objective Complete - Mini Debriefing

We have added a new action to the site that is related to an existing model, but uses its own controller. We used a function call from CGridView to display more complex column information, we used CButtonColumn to define a new button action, Request, and we added a condition to hide the button if a request has already been created. For the management piece, we updated the comic book index page to display any pending requests. We included a convenience link, Lend, that quickly updates the borrower, so that we don't have to navigate to the comic book update page, search for the new borrower, and save the changed record.

Defining Roles and Access

This task is mostly about planning. When you add RBAC to your application, you will need to decide how the access to your system will be allocated. A good place to start is to look at the actions in your current system and the roles you think you will need.

Yii defines RBAC in terms of roles, tasks, and operations. An operation is a single action on an object. We will set its name as the object followed by the action. For example, the name for the operation consisting of the action Create on the object Book would be bookCreate. A task is a named collection of operations. For example, you might collect all of the user management operations (userCreate, userDelete, userUpdate, and userView) into a single task named manageUser. A role is a collection of tasks and operations and other roles. Assigning one role to another creates a hierarchy and is a convenient way for the user to manage nested levels of access. For example, if you have the roles Reader, Contributor, and Administrator, it makes sense that a contributor will have all of the permissions of a reader, and an administrator will have all of the permissions of a contributor.

Administrator > Contributor > Reader

A user is authorized to perform any of the actions collected under a role that is assigned to him/her.

Engage Thrusters

  1. For this project, we will define roles as follows, in order of decreasing levels of access:

i. Authority – your role, with total access

ii. Administrator – can add and edit book and user entries

iii. Borrower – can view the library list and make requests

iv. Viewer – can view the comic book collection

v. wishlistAccess – can only view the wishlist

  1. Expand this list of roles in terms of the actions they can perform:

i. Authority: Create/read/update/delete role, task, operation

ii. Administrator: Create/read/update/delete user, book, wish

iii. Borrower: Read book, wish, library, and make library request

iv. Viewer: Read book and wish

v. WishlistAccess: Read wish and read/update own user entry

  1. Yii provides a function for scripting and loading all of the roles, tasks, and operations we just defined. First, we will have to set up the database to hold these new entities, and it is pretty simple. All you need are three tables to capture three things:
  • Authorization items
  • Authorization hierarchy
  • Assignment
  1. Go to the Servers tab, and use the Execute Command tool to create the following tables (the table definitions can be found in the directory protected/data/auth_tables.sql):
CREATE TABLE 'auth_item' (
  'name'                  varchar(64) NOT NULL,
  'type'                  int NOT NULL,
  'description'           text,
  'bizrule'               text,
  'data'                  text,
  PRIMARY KEY ('name')
) ENGINE=InnoDB
 
CREATE TABLE 'auth_item_child' (
  'parent'        varchar(64) NOT NULL,
  'child'         varchar(64) NOT NULL,
  PRIMARY KEY ('parent', 'child'),
  FOREIGN KEY ('parent') REFERENCES 'auth_item' ('name') ON 
      DELETE CASCADE ON UPDATE CASCADE,
  FOREIGN KEY ('child') REFERENCES 'auth_item' ('name') ON 
      DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB
 
CREATE TABLE 'auth_assignment' (
  'itemname'      varchar(64) NOT NULL,
  -- NOTE - userid is the format the yii libraries expect
  'userid'        int(10) unsigned,
  'bizrule'       text,
  'data'          text,
  PRIMARY KEY ('itemname', 'userid'),
  FOREIGN KEY ('itemname') REFERENCES 'auth_item' ('name') ON 
      DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB

In the next section, we will download and install a Yii extension that will provide a nice graphical interface for RBAC management. Before we do that, we will initialize the RBAC with a script built from the hierarchy we defined in the previous steps. We have included the full script in the chapter files (ch4 | Source Files | protected | command | shell | RbacCommand.php). We will touch on some key excerpts next.

For many of our controllers, we have already created more actions than the Gii-generated CRUD. For example, there are nine operations on the Wish object:

$auth->createOperation('WishAdmin','admin access to wishes');
  $auth->createOperation('WishIndex','index of wishes');
  $auth->createOperation('WishCreate','create a wish');
  $auth->createOperation('WishView','read a wish');
  $auth->createOperation('WishUpdate','update a wish');
  $auth->createOperation('WishDelete','delete a wish');
  $auth->createOperation('WishClaim','claim a wish');
  $auth->createOperation('WishRemoveAuthor','remove an author from a wish');
  $auth->createOperation('WishCreateAuthor','create an author for a wish');

You may notice that the format of the operation name is . The reason for this is that the management module that we will add later relies on this naming convention.

Each role in our hierarchy may be assigned the previous role and any new operations that belong to it.

$role=$auth->createRole('borrower');
$role->addChild('viewer');
$role->addChild('LibraryIndex');
$role->addChild('LibraryRequest');
  1. Update the app configuration file (ch4 | protected | config | main.php) to activate the included Authorization Manager component.
'components'=>array(
    'user'=>array(
        // enable cookie-based authentication
        'allowAutoLogin'=>true,
    ),
    'authManager' => array(
        'class' => 'CDbAuthManager',
        'connectionID' => 'db',
        'assignmentTable' => 'auth_assignment',
        'itemTable' => 'auth_item',
        'itemChildTable' => 'auth_item_child',
    ),
    'urlManager'=>array(
  1. Run the script from the command line with the Yii shell tool to load the hierarchy into the database.
cd ~/project/ch4
yiic shell
>> rbac

The command tool will output Rbac initialized. You can exit the tool with the command exit.

  1. You can verify the initialization by going to the NetBeans server view, connecting to your database, right-clicking on any of the authorization tables, and selecting the View Data command.

Objective Complete - Mini Debriefing

When you implement a role-based access control system, you will need to give some thought to the roles that are needed in your system, the actions each role may perform, and the relationship between the roles. A review of the actions currently in your system can help you generate a list of roles. A diagram of roles and actions can help you spot gaps in your coverage.

Classified Intel

At this point, you could configure your project and move from the file-based authorizations in each controller to the database authorizations that we have initialized, but maintaining and building on this information will be difficult without a more intuitive interface. The Yii team's stated intention was for developers to make suitable RBAC interfaces for their projects. You can create your own, but there are some extensions available that do a good job of meeting most RBAC management needs. So we will install one of those extensions to demonstrate a helpful graphical management interface and then complete the configuration and activate the new authorization system.

Adding the RBAC Extension

In this task, we continue to configure and activate the role-based access control system, and add a management interface to the website to make it easier to review, add to, update, and maintain the authorization information.

Prepare for Lift Off

Download and unpack the srbac and YiiSmartMenu extensions from the Yii website:

    • http://www.yiiframework.com/extension/srbac/
    • http://www.yiiframework.com/extension/yiismartmenu/

Engage Thrusters

  1. Create a directory named modules in your project's protected directory.
cd ~/projects/ch4/protected
mkdir modules
    Move the srbac directory into the newly created modules directory.
mv ~/Downloads/srbac\ 1.3beta/srbac/ ~/project/ch4/protected/modules/.
  1. Edit the project configuration file (ch4 | protected | config | main.php) to import the srbac module.
'import'=>array(
    'application.models.*',
    'application.components.*',
    'application.modules.srbac.controllers.SBaseController',
),
  1. In the same config file, change the AuthManager component to use the srbac module.
'authManager' => array(
    'class' => 'application.modules.srbac.components.SDbAuthManager',
    'connectionID' => 'db',
  1. Also in the config file, add an entry for srbac to the modules array. This entry will configure the behavior of the module.
'srbac' => array(
    'userclass'=>'User', //default: User
    'userid'=>'id', //default: userid
    'username'=>'username', //default:username
    'delimeter'=>'@', //default:-
    'debug'=>true, //default :false
    'pageSize'=>10, // default : 15
    'superUser' =>'Authority', //default: Authorizer
    'css'=>'srbac.css', //default: srbac.css
    'layout'=>
    'application.views.layouts.main', 
    'notAuthorizedView'=> 'srbac.views.authitem.unauthorized', 
    'alwaysAllowed'=>array( 'SiteLogin','SiteLogout','SiteIndex', 'SiteError'),
    'userActions'=>array('Show','View','List'), 
    'listBoxNumberOfLines' => 15, //default : 10
    'imagesPath' => 'srbac.images', // default: srbac.images
    'imagesPack'=>'noia', //default: noia
    'iconText'=>true, // default : false
    'header'=>'srbac.views.authitem.header', 
    'footer'=>'srbac.views.authitem.footer', 
    'showHeader'=>true, // default: false
    'showFooter'=>true, // default: false
    'alwaysAllowedPath'=>'srbac.components', 
),

The following points should be noted in this configuration:

    • For the first run, we leave debugging on, which allows every user access to all screens. We leave the access to srbac open until we have used the interface to configure our admin user to have the authority permission.
    • The alwaysAllowed line lists actions that are public on your site. Put items in this list that you want to be always accessible and do not wish to configure further.
  1. Create a file (ch4 | Source Files | protected | modules | srbac | components | allowed.php) that contains an empty array.
<?php
    return array();
?>

Set the permissions on this file to permit writing from the web server.

chown lomeara:www-data allowed.php

You will be able to configure the contents of this file via the srbac web interface.

  1. Remove the install folder from the project (ch4 | Source Files | protected | modules | srbac | views | authitem | install), because it provides access to administrative functions that are no longer needed.
  2. Now access the srbac page http://localhost/cbdb/index.php/srbac.
  3. Click on the Managing auth items button. The screen will update with a list of the Roles, Operations, and Tasks that were created by our script.
  1. Click on the Create button next to the Search bar and in the form that appears, set Name to Authority, Type to Role, and Description to srbac role.
  1. Click on the Create button in the form to save the new role.
  2. Click on the Assign to users button.
  1. Select the user(s) who will have access to srbac from the left-hand column. For our examples, select the username admin. Select Authority in the right-hand column, and click on the left arrow button [ ] to move from Not Assigned Roles to Assigned Roles. As a result, the user, admin, will have the roles admin and Authority in the Assigned Roles column.
  1. Now you can go back into the config file (ch4 | Source Files | protected | config | main.php) and comment out the srbac debug line, so that only the admin user can access srbac.
  2. For our own convenience, we will add an srbac link to the site menu. Add the following line to ch4 | Source Files | protected | views | layouts | main.php:
array('label'=>'Srbac', 'url'=>array('/srbac'), 'authItemName' => 'Authority'),

For this menu item, we assign the authorized role value, Authority, to authItemName to check the access instead of depending on the URL, because srbac permits access based on this role and does not, as we have configured it, have the operations defined to match the URLs.

  1. To display the applicable contents of the Comic Book menu to all users, we apply this method to the comic book item in the site menu in ch4 | Source Files | protected | views | layouts | main.php. Our lowest level access user, and all users up the hierarchy, should be able to see some items in the Comic Book menu, so we include the lowest level role as the value for authItemName.
array(
    'label'=>'Comic Books',
    'url'=>array('/book/index'),
    'items' => array(
        array('label'=>'Publishers', 'url'=>array('/publisher/index')),
        array('label'=>'WishList', 'url'=>array('/wish/index')),
        array('label'=>'Library', 'url'=>array('/library/index')),
    ),
    'authItemName' => 'WishlistAccess',
),

The Comic Book link will be visible and active, but if a user without authorization selects it, he will receive an error message.

  1. The site currently uses a component, AuthMenu, that displays menu items based on authorization. We built this component in a previous chapter based on a distributed component named YiiSmartMenu to use the file-based authorization configuration. Now we will replace that component with YiiSmartMenu. Copy the file from the extension directory into your project.
cp ~/Downloads/<Yii Smart Menu directory>YiiSmartMenu.php ~/projects/ch4/protected/components/.
  1. Change the value of the member variable partItemSeparator in the YiiSmartMenu class from "." to "" in ch4 | Source Files | protected | components | YiiSmartMenu.php.
public $partItemSeparator = "";
  1. Remove the old AuthMenu file (ch4 | Source Files | protected | components | AuthMenu.php) from the project.
  2. Replace the occurrences of AuthMenu in ch4 | Source Files | protected | views | layouts | column2.php and ch4 | Source Files | protected | views | layouts | main.php with YiiSmartMenu.
  3. Activate the srbac by changing the parent class of ch4 | Source Files | protected | components | Controller.php to SBaseController.
class Controller extends SBaseController
  1. Remove or comment out the accessControl filter in every controller:
    • BookController
    • LibraryController
    • PublisherController
    • UserController
    • WishController
public function filters()
{
    return array(
        //'accessControl', // perform access control for CRUD operations
    );
}
  1. Remove or comment out the accessRules function in every controller, because it is no longer needed.
  2. Try out access control by logging in as a user with fewer privileges. See the Classified Intel section for more information about test users that are already in the database.

Objective Complete - Mini Debriefing

In this section, we explained how to install, configure, and activate the srbac extension. The extension comes with its own set of instructions, but we wanted to offer a more detailed explanation to help you set it up. Also, our approach to the database tables, naming of roles, and organization of the authorization hierarchy is a little different. The configuration example we provided is slightly different from the default.

Classified Intel

You may have noticed in the initial RBAC configuration or when browsing the srbac interface that some users have been assigned to the various roles. We supplied these users in the schema for the chapter with the password for each set to test. We configured each one to a different role, so that you can test user experience for each role. The full list of users to roles is:

  • admin => admin
  • borrower => borrower
  • afriend => viewer
  • twg => wishlistAccess (twg is short for "the wish giver")

Adding Roles to User Management

As you can see, the interface provided by the srbac extension is very powerful and convenient. It is great for adding new roles, tasks, and operations. However, when we add new users to the system, it would be convenient to assign the users, roles on the spot.

Engage Thrusters

  1. First, add the relationship to the assignment object in the User model (ch4 | Source Files | protected | models | User.php).

i. Add a comment at the top of the file listing the new model variable in the model relations section as follows:

* The following are the available model relations:
* @property Person $person
* @property Assignments[] $assignments

ii. Add an entry for assignments to the relations array as follows:

return array(
    'person' => array(self::BELONGS_TO, 'Person', 'person_id'),
    'assignments' => array(self::HAS_MANY, 'Assignments', 'userid'),
);
  1. Display the assignments in the User form (ch4 | Source Files | protected | views | user | _form.php):
<div class="row">
    <ul>
        <?php foreach($user->assignments as $a) {
             echo "<li>" . $a->itemname . "</li>";
        } ?>    
    </ul>
</div>

The currently assigned roles will now appear in the User edit screen as shown in the following screenshot:

  1. Now we need to display the roles that have not been assigned to this user, and provide a means to add an assignment.

Change the list display to use a renderPartial function as follows:

<div class="row">
    <b>Assignments</b><br/>
    <ul class="roles">
        <?php foreach($user->assignments as $a) {
            echo $this->renderPartial('//includes/role_li',
                array(
                    'user' => $user,
                    'assignment' => $a,
            ));
        } ?>
    </ul>
  1. Create a new view file for the renderPartial function call in the directory ch4 | Source Files | protected | views | includes | role_li.php with the following contents:
<?php
echo "<li id=\"role-" . $assignment->itemname. "\">" .
    $assignment->itemname . 
    " <input class=\"revoke\" type=\"button\" " . 
    "onClick=\"revoke('" .
    Yii::app()->controller->createUrl("/user/revokeRole",
        array("id" => $user->id,
            "role_name"=>$assignment->itemname,
            "ajax"=>1)) . "', '" . 
    $assignment->itemname . "')\" " . 
    "value=\"Revoke\" " .
    "/>" .  
    "</li>";
  1. Add a section to display a picklist of unassigned roles:
<b>Un Assigned Roles</b><br/>
<?php echo SHtml::activeDropDownList($user,'role',
    SHtml::listData(
        $user->getUnassignedRoles(), 'name', 'name'),
        array('size'=>5, 'class'=>'dropdown')); ?>
<br/>
<input class="add" type="button" 
    obj="User" 
    url="<?php echo Yii::app()->controller->createUrl(
        "/user/assignRole",
        array("id"=>$user->id)); ?>"
    value="Add"/>
  1. Create a new class variable in the User model to hold the role value as follows:
public $person_fname;
public $person_lname;
public $role;
  1. Add a function that wraps the srbac helper function getUserNotAssignedRoles to return the list of roles that have not been assigned to this user.
public function getUnassignedRoles()
{
    return(Helper::getUserNotAssignedRoles($this->id));
}
  1. Now on the User edit screen, we can choose from a list of unassigned roles, to assign to the user.
  1. Create the assign role action in the User controller.
public function actionAssignRole($id)
{
    // request must be made via ajax
    if(isset($_GET['ajax']) && isset($_GET['role'])) {
        $model=$this->loadModel($id);
        $auth = Yii::app()->authManager;
        $role =CHttpRequest->getParam('role');
        $auth->assign($role, $id, '', '');
        $role=Assignments::model()->find("itemname='" . $role . "'");
        $this->renderPartial('//includes/role_li',array(
            'user'=>$model,
            'assignment'=>$role,
        ), false, true);
    }
    else
        throw new CHttpException(400,'Invalid request.');
}
  1. Create the revoke role action in the User controller.
public function actionRevokeRole($id)
{
    // request must be made via ajax
    if(isset($_GET['ajax'])) {
        $auth = Yii::app()->authManager;
        $auth->revoke($_GET['role_name'], $id);
    }
    else
        throw new CHttpException(400,'Invalid request.');
}
  1. Create a JavaScript file containing these functions to support the assign and revoke buttons.
function revoke(url, role_name) {
    var role = "#role-" + role_name;
    $.ajax({
        type: 'get',
        url: url,
        success: function(resp) {
            $('li').remove(role);
        },
        error: function() {
            alert('error!');
        }
    });
}
 
$(document).ready(function() {
    $('.assign').click(function()$('.assign').click(function() {
        // initialize data object - it's ajax
        data = {
            "role" : $("#User_role").val(),
            "ajax" : "1"
        };
        $.ajax({
            type: 'get',
            url: $(this).attr('url'),
            data: data,
            success: function(resp) {
                $("ul.roles").append(resp);
            },
            error: function() {
                alert('error!');
            }
        }); 
    });
});
  1. Add clientScript calls to the beginning of update.php and create.php in the User view directory (ch4 | Source Files | protected | views | user) to include jQuery and the new custom script.
Yii::app()->getClientScript()->registerCoreScript( 'jquery.ui' );
Yii::app()->clientScript->registerScriptFile(
  Yii::app()->request->baseUrl . '/js/user_form_ajax.js'
);
  1. Go to the srbac screen to add these new actions and assign them to the admin role, so that access to them is limited. On the srbac main screen, click on the Managing auth items button, and then click on the Create button.
  1. Enter UserAssignRole as the name. Leave Type as Operation. Enter a description, if you like. Click on the Create button in the Create New Item form when you are done.
  2. Click on the Create button on the left-hand side of the screen to commence adding a new operation.
  3. Enter UserRevokeRole as the name. Leave Operation as the type. Enter a description, if you like. Click on Create at the bottom of the form when you are done.
  4. Click on the Assign to Users button at the top of the page.
  5. Go to the Tasks tab, and select manageUser under the Task column.
  6. Select UserAssignRole and UserRevokeRole from the Not Assigned Operations column and click on the left arrow button [] to move Operations to the Assigned Operations column.
  7. The actions to assign and revoke work. They update the list of assigned roles, but they do not update the select list of unassigned roles.
  8. To add this feature, start by moving the select list out of the _form.php view and into its own view in ch4 | Source Files | protected | views | includes | role_select.php.
<div id="role_list">
    <b>Un Assigned Roles</b><br/>
    <?php echo CHtml::activeDropDownList($user,'role',
        SHtml::listData($user->getUnassignedRoles(), 'name', 'name'),
        array('size'=>5, 'class'=>'dropdown')); ?>
    <br/>
    <input class="assign" type="button" 
        onClick="assign('<?php echo Yii::app()->controller->createUrl(
            "/user/assignRole",
            array("id"=>$user->id)); ?>','<?php echo Yii::app()->controller->createUrl(
                "/user/reloadRoles",
            array("id"=>$user->id)); ?>')"
        value="Add"/>
</div>

We wrap it in a div tag with the ID role_list so we can easily replace it when we assign or revoke a role.

The assignment row in ch4 | Source Files | protected | views | user | _form.php should now look like the following code snippet:

<div class="row">
    <b>Assignments</b><br/>
    <ul class="roles">
    <?php foreach($user->assignments as $a) {
        echo $this->renderPartial('//includes/role_li',
            array(
                'user' => $user,
                'assignment' => $a,
            ));
    } ?>
    </ul>
 
    <?php echo $this->renderPartial('//includes/role_select',
        array(
            'user' => $user,
    ));
    ?>
</div>
  1. Add a new JavaScript function in ch4 | Source Files | js | user_form_ajax.js to handle the update.
function update_roles(updateUrl) {
    // reload the role select field
    $.ajax({
        type: 'get',
        url: updateUrl,
        data: {
            "ajax" : "1"
        },
        success: function(resp) {
            $("#role_list").replaceWith( resp );
        },
        error: function() {
            alert('Error!');
        }
    });
}
  1. Also add a new parameter to the revoke function to pass in updateUrl and make a call to update_roles when a revoke is successful.
function revoke(url, role_name, updateUrl) {
    success: function(resp) {
    $('li').remove(role);
    update_roles(updateUrl);
},
  1. Remove the $(document).ready call and move the assign function out into a named function. Pass in the values for url and updateUrl.
function assign(url, updateUrl) {
    $.ajax({
        type: 'get',
        url: url,
        data: {
            "role" : $("#User_role").val(),
            "ajax" : "1"
        },
        success: function(resp) {
            $("ul.roles").append(resp);
            update_roles(updateUrl);
        },
        error: function() {
            alert('Error!');
        }
    });
}
  1. Change the role list item view ch4 | Source Files | protected | views | includes | role_li.php to include an onClick call to the revoke function and pass the url variables.
<?php
echo "<li id=\"role-" . $assignment->itemname. "\">" .
    $assignment->itemname .
    " <input class=\"revoke\" type=\"button\" " .
    "onClick=\"revoke('" .
    Yii::app()->controller->createUrl("/user/revokeRole",
        array("id" => $user->id,
            "role_name"=>$assignment->itemname,
            "ajax"=>1)) . "', '" .
    $assignment->itemname . "', '" .
    Yii::app()->controller->createUrl("/user/reloadRoles",
        array("id" => $user->id)) .
        "')\" " .
    "value=\"Revoke\" " .
    "/>" .
    "</li>";
  1. Add the new reload action to the User controller.
public function actionReloadRoles($id)
{
    if(isset($_GET['ajax'])) {
        $model=$this->loadModel($id);
        $this->renderPartial('//includes/role_select',array(
            'user'=>$model,
        ), false, true);
    }
    else
        throw new CHttpException(400,'Invalid request.');
}
  1. In the srbac interface, create an operation for UserReloadRoles and assign it to the manageUser task, as we just did for UserAssignRole and UserRevokeRole.

Now the Assign and Revoke buttons should fully work the way we expect them to.

Objective Complete - Mini Debriefing

In this task, we added a role assignment section to the User edit page. In the process, we created new controller actions to assign and revoke roles, and we used the srbac interface to limit the role assignment operations to the admin role.

Classified Intel

There is an alternate way to add operations to the system. This is particularly useful if you have added many new actions and want to create the corresponding operations all at once.

  1. Go to the srbac screen.
  2. Click on Managing auth items.
  3. Click on the link named Autocreate Auth Items below the buttons.
  4. Click on the lightbulb next to the controller that has new actions; for this task, that would have been UserController.
  5. The list of actions without corresponding operations will be displayed. Click on the ones you want to add. You probably want to uncheck the box for Create Tasks, because we already have some task grouping configured.
  6. You can go back to the main AuthItem management page to verify your new operations by clicking on the Managing auth items button or by clicking on the Manage AuthItem link underneath the buttons.

Fine-tuning Permissions

In relatively few steps, we have applied a finer grained access control to our site, but there may be one or two very tiny-grained areas that we have overlooked. In this task, we will clean up some access issues and, in the process, look at methods of applying even smaller areas of access control.

Engage Thrusters

  1. One area not completely covered by access control is the comic book index. We display the list of requests. The admin, borrower, and viewer roles have access to this page, but only admin should be able to see the requests. To display the information only to authorized users, we need to add an authorization check. However, we do not want to check in the view itself, because views should not include business logic. Instead, we will perform the authorization check in the controller, within the Book View action, and pass the result to the view. Start by updating the index view (ch4 | Source Files | protected | views | book | index.php) to pass a new variable named isAdmin to the ListView widget.
<?php $this->widget('zii.widgets.CListView', array(
    'dataProvider'=>$dataProvider,
    'viewData' => array('isAdmin' => $isAdmin),
    'itemView'=>'_view',
)); ?>
  1. Update ch4 | Source Files | protected | views | book | _view.php to check this variable.
<?php
if ($data->requesters && $isAdmin) {
    echo "<b>Requests</b><br/>\n";
  1. In the Book controller (ch4 | Source Files | protected | controllers | BookController.php), perform the access control check, set the variable, and pass it to the view.
$this->render('index',array(
    'dataProvider'=>$dataProvider,
    'isAdmin'=>Yii::app()->user->checkAccess('admin'),
));
  1. We need to make the same change for the User edit screen, since we just expanded access to let users edit their own profiles. You probably don't want to allow them to choose their own level of access.

i. Follow almost the same steps. Add the isAdmin field to the render call in the User Controller Update and Create actions.

$this->render('update',array(
    'model'=>$model,
    'isAdmin'=>Yii::app()->user->checkAccess('admin'),
));

ii. Pass the value to the _form render in the User Update and User Create views.

<?php echo $this->renderPartial('_form', array(
    'user'=>$model, 
    'person' => $model->person,
    'isAdmin' => $isAdmin,
)); ?>

iii. Finally, in the User _form view, wrap the assignments row in a check for isAdmin.

<?php if ($isAdmin) { ?>
<div class="row">
    <b>Assignments</b><br/>
    <ul class="roles">
    <?php foreach($user->assignments as $a) {
        echo $this->renderPartial('//includes/role_li',
            array(
                'user' => $user,
                'assignment' => $a,
            ));
        } ?>
    </ul>
 
    <?php echo $this->renderPartial('//includes/role_select',
        array(
            'user' => $user,
        ));
    ?>
</div>
<?php } ?>
  1. The default authorization error message provides some details about the action that was unauthorized. What if we want to present the error with less detailed information? Start by creating a folder in the views directory.
cd ~/projects/ch4/protected/views
mkdir srbac

We named the directory srbac to associate the views in the directory with the module that will use them.

  1. Create a file named access_denied.php in the new directory.
  2. For our example, we will output a simple HTML error message.
<h2 style="color:red">Access denied.</h2>
  1. Change the value of notAuthorizedView in the srbac configuration (ch4 | Source Files | protected | config | main.php) to point to the view we just created.
'notAuthorizedView'=> 'application.views.srbac.access_denied',
  1. You may have noticed that one of the tasks that we have created is named UpdateOwnUser. In the RBAC initialization script, the entry looks like this:
// user task of updating own entry
$bizRule='return (Yii::app()->user->id==Yii::app()->getRequest()->getQuery('id') || Yii::app()->user->id == $params['id']);';
$task=$auth->createTask('UpdateOwnUser','update own user entry',$bizRule);
$task->addChild('UserUpdate');
  1. To give users access to edit their own user record, we have to create a point of entry, as usual by creating a menu item in ch4 | Source Files | protected | views | layouts | main.php. We will add the entry to the top-level menu, so that our users can easily find it.
array(
    'label'=>'Edit Profile',
    'url'=>$this->createUrl('/user/update',
        array('id'=>Yii::app()->user->getId())),
    'authItemName' => 'UpdateOwnUser',
    'authParams' => array('id'=>Yii::app()->user->getId()), 
),

All of our roles will now have a quick link to edit the user's profile.

Objective Complete - Mini Debriefing

The srbac extension provides a default level of access control for each action. In this section, we demonstrated some ways to implement even finer-grained access control, such as displaying a portion of a view. We overrode a view in the module.

Classified Intel

There is an alternate way to permit users to edit their own profiles. In this task, we demonstrated the use of a business rule, but you may prefer to keep logic out of your data layer. Another way to achieve the same effect is to perform the following steps:

  1. Limit access to the UserUpdate operation to the admin role only.
  2. Create a User action named Edit Profile that passes the active user ID to the Update action.
public function actionEditProfile()
{
    $this->actionUpdate(Yii::app()->user->id);
}
  1. In srbac, create a new operation for UserEditProfile and assign it to the base role that should be able to edit its own profile.

This approach also effectively limits access to the Update action only to the user's own ID.

Making History

Along with access control comes audit logging . Once you grant more users access to your site, you have a greater need to record the actions users have taken. For example, say your comic book collection is so extensive that you hire some folks to help you input books. Maybe there are consistent errors in the data entry. If you have audit trails, you can identify who is making the errors, and give them more training to input the books with fewer errors. Another common use for audit trails is to retrieve an item that has been accidentally deleted.

Prepare for Lift Off

Download and unpack the auditTrail extension from the Yii website (http://www.yiiframework.com/extension/audittrail/).

Engage Thrusters

  1. Copy the unpacked auditTrail folder into your project's modules folder.
cp ~/Downloads/auditTrail ~/ch4/protected/modules/.
  1. Add the module to the import array in your configuration file (ch4 | Source Files | protected | config | main.php).
'import'=>array(
    'application.models.*',
    'application.components.*',
    'application.modules.srbac.controllers.SBaseController',
    'application.modules.auditTrail.models.AuditTrail',
),

And to the modules array.

 'auditTrail'=>array(),
  1. Replace the entry for db in ch4 | Source Files | protected | config | console.php.
'db'=>array(
    'connectionString' => 'mysql:host=localhost;dbname=cbdb',
    'emulatePrepare' => true,
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
),
  1. Edit the file yiic.php in the project root. Change the path to yiic to the absolute path.
$yiic='/opt/lampp/htdocs/yii-1.1.10.r3566/framework/yiic.php';
  1. Change the model and field values in ch4 | Source Files | protected | modules | auditTrail | migrations | m110517_155003_create_tables_audit_trail.php as follows:
'model' => 'string NOT NULL',
'field' => 'string NOT NULL',

Comment out the index lines for old_value and new_value.

//$this->createIndex( 'idx_audit_trail_old_value', 'tbl_audit_trail', 'old_value');
//$this->createIndex( 'idx_audit_trail_new_value', 'tbl_audit_trail', 'new_value');
  1. Run the migration script supplied by the auditTrail extension.
cd ~/projects/ch4/protected
php ./yiic.php migrate --migrationPath=application.modules.auditTrail.migrations

When the script asks you if you want to apply the changes, say Yes.

  1. In srbac, create an operation named auditTrail@AdminAdmin and a manageAuditTrail task.
  2. In the Tasks tab under Assign to users, assign the auditTrail operation to the manageAuditTrail task.
  3. Under the Roles tab, assign the manageAuditTrail task to the admin role.

Access the new page at http://localhost/cbdb/index.php/auditTrail/admin.

  1. Add the Audit Trail Management page to ch4 | Source Files | protected | views | layouts | main.php so the authority user can see the link.
array('label'=>'AuditTrail', 'url'=>array('/auditTrail/admin'),
    'authItemName' => 'Authority'),
  1. To try out the audit capture, add the following function to the User model.
public function behaviors() 
{ 
    return array( 'LoggableBehavior'=> 'application.modules.auditTrail.behaviors.LoggableBehavior', ); 
}
  1. Add or edit a user and then go to the Audit Trail Management page. You will see a searchable list of the changes you have made.

Objective Complete - Mini Debriefing

We installed and configured an audit trail extension to capture changes to our records.

Classified Intel

You can also add an audit trail widget to a view to show changes to an individual record. An example of this can be illustrated by adding the following snippet at the end of the User view file ch4 | Source Files | protected | views | user | view.php:

<?php 
$this->widget(
    'application.modules.auditTrail.widgets.portlets.ShowAuditTrail',
    array(
        'model' => $model,
    )
);
?>

Mission Accomplished

We have added two new features to the site: a library view for our users and library management utilities for us. To support these features and the different users of our system, we changed our access control scheme from the default access control filter to role-based access control. We installed the srbac extension to make management of the RBAC configuration easier, and we installed the AuditTrail extension to record data changes.

Remember – if you put this site online, review your security and definitely disable Gii in the configuration.

You Ready to go Gung HO? A Hotshot Challenge

Here are some ideas to go gung ho with user functions:

  • Expand your library system by adding a Request a Book or Suggest a Book function.
  • Generalize the auto-complete function by creating an action extension.
  • Make the Book admin page editable so that you can manage your library from one screen, instead of finding a comic book and then clicking on edit to update the entry.
  • Create a quick entry row on the Book admin page so that you can quickly input new books with less clicking.
  • Add an illustrator column to the library page.
  • Create management and authorization for other entities such as publisher.
  • Add a Withdraw Request function to the library grid.
  • An alternate approach to permissions is an Access Control List implementation, which is appropriate when you want individual permissions versus group permissions. You could apply an ACL extension to the baseline chapter files to compare the different approaches.
评论 X

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