[CakePHP] Access Control List (p2)

phần một tôi đã trình bày nội dung lý thuyết của Access Control List - ACL. Có thể đọc qua sẽ khó hiểu ngay được khi vào thực tế sẽ áp dụng như thế nào nên trong phần này tôi sẽ tiếp tục viết về ACL bằng một ứng dụng web mini. Tôi sẽ xây dựng một website để viết blog sử dụng kết hợp Auth và ACL đi từ những bước đầu tiên với code được đơn giản nhất có thể.

Sơ lược demo

Như các bạn đã biết ở phần trước ACL hoạt động theo dạng cây với nhiều tầng lớp các node Access Request Object - ARO và Access Control Object - ACO. ACO - cơ bản thì demo sẽ có các chức năng đơn giản về User và Post như là view, tạo, sửa, xóa. ARO - trong demo này tôi sẽ phân theo nhóm quyền như sau :

  • Admin : sẽ là người có quyền cao nhất với cả User và Post.
  • Mod : sẽ là người có toàn quyền với các Post. Nhưng đối với User sẽ chỉ có quyền view list và chi tiết của các user.
  • User : sẽ có các quyền view list, thêm và sửa Post.

Về Database

Chúng ta đã có mô hình phân quyền, giờ là tạo ra các AROs - Groups và Users. Bảng chỉ cần đơn giản như sau là đủ :

CREATE TABLE users (
    id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(255) NOT NULL UNIQUE,
    password CHAR(40) NOT NULL,
    group_id INT(11) NOT NULL,
    created DATETIME,
    modified DATETIME
);

CREATE TABLE groups (
    id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    created DATETIME,
    modified DATETIME
);

Kế đó là bảng sẽ lưu trữ nội dung các bài blog do user đăng lên :

CREATE TABLE posts (
    id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    user_id INT(11) NOT NULL,
    title VARCHAR(255) NOT NULL,
    body TEXT,
    created DATETIME,
    modified DATETIME
);
Tip : sau khi đã tạo các bảng trên và khai báo các thông số kết nối tới DB chứa chúng thì có thể dùng Bake để tạo ra model, controller và view nhanh chóng hơn chỉ trong vài nốt nhạc :)

Cách sử dụng Bake như dưới đây bằng command line :

// bạn cần cd đến thư mục demo dạng
cd /path/to/demo/app
// sử dụng cake shell chạy lệnh sau
Console/cake bake all

Với lệnh trên bạn sẽ nhận được thông báo tương tự như bên dưới cho biết bạn có thể tạo (Bake) nhanh được các model nào. Việc của bạn chỉ việc gõ số tương ứng và enter. Bạn chạy lần lượt cho cả 3 bảng trên thì sẽ có đầy đủ các Model, Controller và View tương ứng với chúng :

Possible Models based on your current database:
1. Group
2. Post
3. User
Enter a number from the list above,
type in the name of another model, or 'q' to exit
[q] >

Chú ý là bạn có thể bị hỏi có tạo ra các file test hay không, chúng ta sẽ không tạo ra các file này nên sẽ nhập n và enter.

PHPUnit is not installed. Do you want to bake unit test files anyway? (y/n)
[y] >

Sử dụng Auth

Sau khi đã gen tự động được các file, chúng ta bắt đầu thực hiện thêm phần Auth. Đầu tiên là login :

// thêm trong user controller
    public function login() {
        if ($this->request->is('post')) {
            if ($this->Auth->login()) {
                return $this->redirect($this->Auth->redirectUrl());
            }
            $this->Session->setFlash(__('Incorrect your username or password.'));
        }
    }
// add file view tương ứng cho login()
    <?php
    echo $this->Form->create('User', array('action' => 'login'));
    echo $this->Form->inputs(array(
        'legend' => __('Login'),
        'username',
        'password'
    ));
    echo $this->Form->end('Login');
    ?>

Sau đó là thêm xử lý mã hóa chuỗi mật khẩu nhập vào vì không ai để text thô vào DB cho pass cả :

    public function beforeSave($options = array()) {
        $this->data['User']['password'] = AuthComponent::password(
          $this->data['User']['password']
        );
        return true;
    }

Kế đến là chỉnh sửa file /app/Controller/AppController.php. Do chúng ta dùng đến Acl và Auth cho tất cả các page nên khai báo ở đây và các controller khác sẽ kế thừa, tương tự với các Helpers. Ngoài ra, chúng ta thiết lập cho phép user login bằng Auth, chuyển hướng họ khi đăng nhập hay đăng xuất. Tôi đã thêm đoạn sau vào /app/Controller/AppController.php :

/**
 * chúng ta sử dụng Acl và Auth cho tất cả nên ở phần tiếp sẽ có định nghĩa root ACO = controllers cho dễ quản lý
 */
    public $components = array(
        'Acl',
        'Auth' => array(
            'authorize' => array(
                'Actions' => array('actionPath' => 'controllers')
            )
        ),
        'Session'
    );

/**
 * Cần khai báo thêm việc sử dụng các helpers hữu ích cần dùng đến
 */
    public $helpers = array('Html', 'Form', 'Session');

/**
 * Config AuthComponent
 */
    public function beforeFilter() {
        // cho phép user login
        $this->Auth->loginAction = array(
          'controller' => 'users',
          'action' => 'login'
        );
        // khi user log out sẽ chuyển hướng đến trang login
        $this->Auth->logoutRedirect = array(
          'controller' => 'users',
          'action' => 'login'
        );
        // khi user đã login thì chuyển hướng đến trang list post
        $this->Auth->loginRedirect = array(
          'controller' => 'posts',
          'action' => 'index'
        );
    }

Do chúng ta mới chỉ cho phép login chứ chưa cho user làm bất cứ gì khác nên để tạo ra các group, user thì chúng ta phải cho phép điều này. Tôi đã thêm đoạn code sau vào cả GroupsControllerUsersController :

public function beforeFilter() {
    parent::beforeFilter();
    // hàm allow() mặc định cho phép sau khi login user có thể làm tất cả các actions
    $this->Auth->allow();
}

Khởi tạo DB ACL

Như trong bài trước đã đề cập đến cake shell, chúng ta sẽ chạy command sau để khởi tạo DB ACL :

./Console/cake schema create DbAcl
Tip : nếu bạn không chạy được cake shell thì có thể tìm thấy file db _acl.sql_ trong _/app/Config/Schema/_, chạy file này cũng tương tự command trên.

Sau khi chạy thì chúng ta đã có được ACL giúp liên kết các user và group thành một mối liên hệ sẽ giúp chúng ta control ứng dụng web của mình.

Thiết lập ACL

Các AROs - Requesters

Chúng ta sẽ cần liên kết các user và group bằng cách đưa chúng vào từng dòng trong bảng ACL, và kế đến ta sẽ sử dụng AclBehavior để làm điều đó. Behavior này cho phép tự động kết nối các models với các bảng ACL. Do đó, trong User model (User.php) tôi đã thêm đoạn sau :

// khai báo $actAs để chấp nhận các request trong ACL từ các ARO
    public $actsAs = array('Acl' => array('type' => 'requester'));
// sử dụng parentNode() để biết được node cha (group) của của user
    public function parentNode() {
        if (!$this->id && empty($this->data)) {
            return null;
        }
        if (isset($this->data['User']['group_id'])) {
            $groupId = $this->data['User']['group_id'];
        } else {
            $groupId = $this->field('group_id');
        }
        if (!$groupId) {
            return null;
        }
        return array('Group' => array('id' => $groupId));
    }

Tương tự, tôi cũng đã thêm đoạn code dưới vào Group.php

    public $actsAs = array('Acl' => array('type' => 'requester'));
    // do group ko có cha nên ta luôn trả về null
    public function parentNode() {
        return null;
    }

Việc sử dụng code như trên sẽ giúp chúng ta quản lý được việc tạo hay sửa xóa user hoặc group được dễ dàng hơn, cụ thể là dữ liệu ở bảng ARO cũng sẽ được update theo.

Đến đây chúng ta đã hoàn tất việc thiết lập kết nối các yếu tố cần thiết nên có thể bắt đầu tạo các GroupsUsers trong các nhóm đã tạo. Và ở bảng aros bạn sẽ nhìn thấy dữ liệu cũng đã được insert theo :

id parent_id model foreign_key alias lft rght
1 NULL Group 1 NULL 1 4
2 NULL Group 2 NULL 5 8
3 NULL Group 3 NULL 9 12
4 1 User 1 NULL 2 3
5 2 User 2 NULL 6 7
6 3 User 3 NULL 10 11

Với dữ liệu như trên thì chúng ta có thể phân quyền ACL đến chi tiết từng User nhưng để đơn giản hóa demo và cũng là biết cách dùng, tôi sẽ chỉ cho phép phân quyền đến mức group như đầu bài tôi đã nói. Để làm thế, chúng ta cần dùng bindNode() trong User.php model :

// chúng ta sẽ nối các node dựa vào khóa ngoại
public function bindNode($user) {
    return array('model' => 'Group', 'foreign_key' => $user['User']['group_id']);
}

// ngoài ra chúng ta cũng cần vô hiệu hóa các request từ user bằng cách sửa biến $actAs
public $actsAs = array('Acl' => array('type' => 'requester', 'enabled' => false));

Giờ nếu bạn add thêm một user thì sẽ thấy không có thêm dòng nào trong bảng aros nữa, đó là vì ACL đã không còn kiểm tra đến User ARO nữa.

Tip : để tiếp tục bạn hãy xóa dữ liệu các bảng aros, groups và users đi và tạo lại từ đầu vì ở lần thêm trước chúng ta đã có tạo cả User ARO. Và chúng ta sẽ có dữ liệu như dưới :
id parent_id model foreign_key alias lft rght
7 NULL Group 4 NULL 1 2
8 NULL Group 5 NULL 3 4
9 NULL Group 6 NULL 5 6

Các ACOs

Như các bạn đã thấy thì khi chúng ta tạo các Groups và Users thì ARO sẽ tự động được tạo ra nhưng với ACOs thì chúng ta không làm được vậy. Chúng ta sẽ có 2 cách để tạo ra ACOs : một là dùng cake shell, hai là dùng AclComponent như trong phần trước tôi đã đề cập. Ý tưởng là chúng ta sẽ tạo ra một ACO gốc cho mọi ACO khác, mục đích để dễ dàng cấp quyền cho toàn bộ các node con bên trong nó - chúng ta sẽ đặt alias cho ACO gốc đó là controllers bởi vì trong đoạn code bên trong chúng ta đã khai báo 'actionPath' => 'controllers' trong biến $component. Do chỉ có một ACO gốc nên tôi sẽ dùng cake shell :

sudo ./Console/cake acl create aco root controllers

Thiết lập quyền

Cũng giống như việc tạo ACOs thì việc thiết lập quyền không được tự động như ARO nên chúng ta cần làm nó thủ công. Và cũng có hai cách là dùng đến shell và AclComponent. Lần này tôi sẽ dùng AclComponent làm ví dụ :

/***
 * chúng ta sẽ tạm thời dùng phương thức setPermissons()
 * sau đó truy cập vào nó trên trình duyệt để thiết lập
 * khi xong thì ta sẽ xóa đi
 */
    public function setPermissons() {
        $group = $this->User->Group;

        // cho phép admin làm mọi thứ
        $group->id = 4;
        $this->Acl->allow($group, 'controllers');

        // cho phép moderator làm mọi việc với post nhưng với user thì chỉ được view list và xem chi tiết ttin user
        $group->id = 5;
        $this->Acl->deny($group, 'controllers');
        $this->Acl->allow($group, 'controllers/Posts');
        $this->Acl->allow($group, 'controllers/Users/index');
        $this->Acl->allow($group, 'controllers/Users/view');

        // chỉ cho phép user được thêm, edit và view list post
        $group->id = 6;
        $this->Acl->deny($group, 'controllers');
        $this->Acl->allow($group, 'controllers/Posts/add');
        $this->Acl->allow($group, 'controllers/Posts/edit');
        $this->Acl->allow($group, 'controllers/Posts/index');

        // cho phép user logout
        $this->Acl->allow($group, 'controllers/Users/logout');

        // do ko có view của phương thức này nên ta sẽ làm cái trick để tránh báo lỗi thiếu view
        echo "ok now";
        exit;
    }

Ngoài ra, hãy truyền tham số cho allow() trong phương thức beforeFilter() của UsersController.php

$this->Auth->allow('setPermissons');

Đến đây một khi chạy setPermissons() thì quyền đã được thiết lập như trên, nhưng với quyền public thì user nên được view chi tiết từng post. Do đó, tôi đã thêm đoạn code sau vào PostsController.php.

public function beforeFilter() {
    parent::beforeFilter();
    $this->Auth->allow('view');
}

Cuối cùng trong AppController.php hãy thêm vào dòng code sau trong phương thức beforeFilter() :

$this->Auth->allow('display');

Hoàn thiện

Bước cuối cùng để hoàn thiện demo này, chúng ta sẽ thêm vào phương thức logout() để user có thể thoát khỏi hệ thống.

    // khi user logout sẽ thấy msg
    public function logout() {
        $this->Session->setFlash('Thanks & see you again !');
        $this->redirect($this->Auth->logout());
    }

Code on github