[轉貼] Access Control List (ACL) with Code Igniter

From: http://jorgealbaladejo.com/2009/04/24/access-control-list-acl-with-code-igniter/

Access Control List (ACL) with Code Igniter

In the last project I’ve worked on, I needed to install any kind of ACL to allow certain methods to be accessed only by some user roles, like website administration, etc. I’m building this website on Code Igniter, so I missed some related features that are available in other PHP frameworks like CakePHP or Zend.

After googling a bit, I found our several methods to get an ACL. Zend framework can be integrated into Code Igniter to use its library, but doesn’t seem really natural to me. I prefer having an previously constructed list, than building it ‘on the fly’ inside my code.

Then I fell into phpGACL, a free software project which allows to solve this problem on an easy way. It uses an API to connect to a database, where tuples of ‘controller’ – ‘method’ – ‘user’ can be stored (actually, AXO – ACO – ARO, acronyms of Access eXtended Object, Access Control Object, Access Request Object). And I developed my first version over this tool, on the most transparent way I was able to find:

  • There is a MY_Controller class where all other controllers inherit from
  • Constructor in this class launches a method, also in the parent controller, like $this->checkAccess(), so ACL is always evaluated before launching any other controller.
  • Method checkAccess gets user ID and role from session values (typically, in database) and calls phpGACL passing it three parameters: the user role, and current method and controller (both taken from the url string)

Note that I used the parent controller constructor to launch the user access control, but I could have used a hook. However, I preferred this for the sake of visibility: I’ll never forget what’s exactly running on every call to my controllers. But, this is only a matter of personal style.

The code would have looked like this:

class MY_Controller extends Controller
{
  var $gacl = NULL;
  function MY_Controller()
  {
    parent::Controller();

    // acl initialization
    include_once('path_to_phpgacl_library');
    $this->gacl = new gacl(array('db_host'     => 'acl_host',
                                 'db_user'     => 'acl_user',
                                 'db_password' => 'acl_pass',
                                 'db_name'     => 'acl_base',
                                 'db_type'     => 'acl_type' ) );
    // does the user have access?
    if (!$this->checkAccess())
    {
      redirect('','refresh');
    }
  }

  /**
     * Uses phpGACL library to determine user access.
     * Checks if a user has access to a method on a controller.
     * Takes user role from session.
     * Takes method and controller from $this->uri attribute.
     *
     * @return boolean if access is granted
     *
     */
  function checkAccess()
  {
    // variables
    $method     = $this->uri->rsegments[2];
    $controller = $this->uri->rsegments[1];
    $role       = $this->rdauth->getUserRole();
    //

    // I've created sections 'methods' for ACO's,
    //  'users' for ARO's and 'controllers' for AXO's
    return ( $this->gacl->acl_check( 'methods', $method,
                                     'users', $role,
                                     'controllers', $controller ) );
  }
}

Note also that this method runs automagically, converting url strings (controller and method) to phpGACL calls. Even though loading the default method for a controller (/index is omitted), internally CI auto fills this value as if it was written in url.

User role, as stated before, is returned by a custom library called rdauth, which will check session cookie to determine if there is a user logged in or not.

I used this system along several weeks, but finally I realized that – for my particular case – having all ACL definition in a database was not a good idea. Although phpGACL has an easy to use web interface to create these lists, and access control was separated from code, I found out these potential problems:

  1. Of course, ACL’s are out of code. That means that it database fails, or any careless user changes something there, all sites whose permissions depend on this database will allow the wrong user roles to access the undesired forbidden controller / methods. Not a good piece of news for paranoid administrators.
  2. Unless I create several databases, all historic branches, develop version and production version will share the same permissions. This is a bit messy to work with.
  3. When I’m coding I don’t have a clear idea of what methods can be accessed by the user role that I’m testing. If I develop a new method to test, I need to go to web admin tool to add it.

So there are benefits of using phpGACL, but also derived problems. Was at this point, when talking to a friend about this, that we realized that actually doesn’t matter if ACL’s are in databases or just arrays.

So the approach of the final solution implemented is:

  • Let’s change phpGACL with an access array in every controller
  • Let’s change checkAccess method to look into this array instead of calling phpGACL API

The main flow for the users access control remains unchanged, and it is still transparent from a coder’s view. Even we gain control over what’s happening with every controller, since $access array is very descriptive:

var $access = array('login'                  => array('visitors') ,
                    'resetPassword'          => array('visitors',
                                                      'registered',
                                                      'editors',
                                                      'managers',
                                                      'administrators') ,
                    'resetPasswordConfirm'   => array('visitors',
                                                      'registered',
                                                      'editors',
                                                      'managers',
                                                      'administrators') ,
                    'changePassword'         => array('registered',
                                                      'editors',
                                                      'managers',
                                                      'administrators') ,
                    'logout'                 => array('registered',
                                                      'editors',
                                                      'managers',
                                                      'administrators') );

Each method in the current controller has an entry in $access array. This entry is an array with all the roles that can load every method. Simple, isn’t it?

MY_Controller class then would look like:

class MY_Controller extends Controller
{
  var $gacl = NULL;
  function MY_Controller()
  {
    parent::Controller();

    // does the user have access?
    if (!$this->checkAccess())
    {
      redirect('','refresh');
    }
  }

  /**
     * Uses every controller access array to determine user permissions.
     * Checks if a user has access to a method on a controller.
     * Takes user role from session.
     * Takes method and controller from $this->uri attribute
     *
     * @return boolean if access is granted
     * @see phpGACL
     *
     */
    function checkAccess()
    {
        // variables
        $method = $this->uri->rsegments[2];
        $role    = $this->rdauth->getUserRole();
        //

        // check access
        if ($method)
        {
            if (array_key_exists($method, $this->access))
            {
                if (in_array($role, $this->access[$method]))
                {
                    return(true);
                }
            }
        }
        return (false);
    }

  }

So now, instead of just calling phpGACL API, I’m going over the access array, checking if there is defined an array of permissions for current method, and then if this array contains the current user role.

I haven’t finished defining users access control yet. There are further checks to forbid, for instance, avoiding that some user edited other users than itself, but these kind of ID checking will be implemented later (and perhaps explained on the following post).

What do you think? Do you use other approaches to get a good users access control list?

Check for Ajax request in Code Igniter

From: http://stackoverflow.com/questions/11687180/check-for-ajax-request-in-code-igniter

Q:

I’m in a PHP script and I want to check whether the request is an Ajax request. (basically so as NOT to allow direct script access, other than Ajax call that is…)

So, I’m defining IS_AJAX somewhere in the main index.php file :

define('IS_AJAX', 
       isset($_SERVER['HTTP_X_REQUESTED_WITH']) && 
       strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest');

And then checking it at the top of my script :

if (!IS_AJAX) exit('No direct script access allowed');

Since I’m new to CodeIgniter, I’m not really sure…

  • Is there any such built-in functionality?
  • Is there a more… elegant way to do it?

A:

You can use $this->input->is_ajax_request() from the input class:

if (!$this->input->is_ajax_request()) {
    exit(‘No direct script access allowed’);
}

php 類別裏如何呼叫自已的method來完成array_map()

參考:http://tw2.php.net/manual/en/language.pseudo-types.php#language.types.callback

To use object methods with array_map(), pass an array containing the object instance and the method name. For same-object scope, use $this as normal. Since your method name is defined in your public $func property, you can pass $this->func. This applies to most functions that accept a callback as an argument.

As a side note, the parentheses outside array_map() aren’t necessary.

return array_map(array($this, $this->func), range($min, $max));

重點的關鍵在使用 $this 變數,以下是我在類別中使用的寫法。這是在類別內相關聯的程式片段

    private $quarters_arr;
    private $item2index;
    private $index2item;
    function __construct(){
        parent::__construct();
        $quarters_arr=array();
        $item2index=array("electric_power"=>1,"derv"=>2,"natural_gas"=>38,"gas"=>3,"LPG"=>39,"tap_water"=>12);
        $index2item=array_flip($item2index);
    }

    function index2item($a){
        $a->item_id=$index2item[$a->item_id];
        return $a;
    }

    function get_quarter_energy_amount($target_quarter){
        $response_json_data=Array();
        $energy_use=$this->db->query("select * from carbon_energy_use_statistics where quarter='{$target_quarter}'");
        $energy_use_array=(array)$energy_use->result();
        $energy_use_array=array_map($this->index2item,$energy_use_array);
        $response_json_data['quarter']=$target_quarter;
        $response_json_data['production']=$this->db->query("select production from quarter_production where quarter='{$target_quarter}';")->row(1)->production;
        $response_json_data['usage_amount']=$energy_use_array;
        return (array)$response_json_data;
    }

codeigniter 類別中自訂義有函式使用「$this->index2item」

$energy_use_array=array_map($this->index2item,$energy_use_array);

nginx codeigniter fast-cgi + virtual-host(它因擾我至少兩天)

/etc/nginx/conf.d/virt.conf

#
# The default server
#
server {
listen 80;
server_name carbonfootprint.toyoink-ct.com.tw;

index carbonfootprint.php;
if (-e $request_filename) {
break;
}
if (!-e $request_filename) {
rewrite ^/(.*)$ /carbonfootprint.php/$1 last;
break;
}
#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root /usr/share/nginx/html;
index carbonfootprint.php ;
try_files $uri $uri/ /carbonfootprint.php;
}

error_page 404 /404.html;
location = /404.html {
root /usr/share/nginx/html;
}

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
location ~* \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index carbonfootprint.php;
fastcgi_split_path_info ^(.+\.php)(.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# deny access to .htaccess files, if Apache’s document root
# concurs with nginx’s one
#
#location ~ /\.ht {
# deny all;
#}
}

Hooks – 擴充核心

Hooks – 擴充核心

CodeIgniter的Hooks機制提供了一個方法來進入並改變框架的內部作業而不用修改核心檔案。當CodeIgniter執行時,它會遵循在程式流程圖頁面中圖示的流程。不過在一些實例中,你也許會想要在流程的特定的階段執行特定的動作。例如,你可能會想要在你的控制器(controller)載入前後執行一些程式,或是在其他位置觸發你自己的程式。

啟動Hooks

Hooks機制可以透過下列 application/config/config.php 檔案中的設定項目來啟動:

$config['enable_hooks'] = TRUE;

定義一個Hook

Hooks定義在 application/config/hooks.php 檔案中。每一個hook都用下面陣列的方式來指定:

$hook['pre_controller'] = array(
'class'    => 'MyClass',
'function' => 'Myfunction',
'filename' => 'Myclass.php',
'filepath' => 'hooks',
'params'   => array('beer','wine','snacks')
);

注意:
陣列索引的名稱與你要使用的特定的hook插入點相對應。在上例中,hook插入點是 pre_controller 。本頁底下可以找到一個hook插入點的清單。另外,以下的項目必須在hook的關係陣列中定義好:

  • class  你想要執行的類別名稱。如果你想要使用一個函數而不是類別,就保持空白。
  • function  你要呼叫的函數名稱。
  • filename  你的類別/函數所在的檔案名稱。
  • filepath  包含你程式的目錄名稱。注意:程式必須在你的 application 目錄內,所以這個目錄名稱是相對於 application 的路徑。例如,如果你的程式放在 application/hooks ,那你只要用 hooks 當作filepath。如果你的程式放在 application/hooks/utilities ,那你要用hooks/utilities 作為filepath。路徑最後不用加斜線。
  • params  任何你希望傳遞給程式的參數,這是非必須的選項。

在同一的Hook中多次呼叫

如果要在一個hook插入多個程式,只要把陣列宣告成多維的就可以。像這樣:

$hook['pre_controller'][] = array(
'class'    => 'MyClass',
'function' => 'Myfunction',
'filename' => 'Myclass.php',
'filepath' => 'hooks',
'params'   => array('beer','wine','snacks')
);

$hook['pre_controller'][] = array(
'class'    => 'MyOtherClass',
'function' => 'MyOtherfunction',
'filename' => 'Myotherclass.php',
'filepath' => 'hooks',
'params'   => array('red','yellow','blue')
);

注意要在陣列索引後加上中括號:

$hook['pre_controller'][]

這樣就可以讓你在一個hook中插入多個程式。程式執行的順序就是你在陣列中定義的順序。

Hook插入點

以下是可用的hook插入點清單:

  • pre_system
    在系統執行的初期執行。這時只有benchmark及hook類別被載入。路由或其他程序都還沒執行。
  • pre_controller
    在所有的控制器(controller)呼叫之前執行。此時所有的基礎類別、路由、安全檢查都已經完成。
  • post_controller_constructor
    在控制器(controller)實例化之後但是任何方法都還未呼叫之前立刻執行。
  • post_controller
    在控制器(controller)執行完畢之後立刻執行。
  • display_override
    覆蓋用來在系統執行完畢後向瀏覽器送出完成的頁面的 _display() 函數。這樣就允許你用你自己定義的顯示方法。注意,你必須用 $this->CI =& get_instance() 取得CI參考物件然後才可透過呼叫 $this->CI->output->get_output() 來使用完成的頁面資料。
  • cache_override
    讓你可以呼叫自己定義的函數而非output類別中的 _display_cache() 函數。這讓你可以使用自己的cache顯示機制。
  • post_system
    在完成的頁面送到瀏覽器之後呼叫。在系統執行結束時,完成的資料已送到瀏覽器之後執行。