phalcon / cphalcon

High performance, full-stack PHP framework delivered as a C extension.
https://phalcon.io
BSD 3-Clause "New" or "Revised" License
10.76k stars 1.98k forks source link

[NFR] RLS (row-level-security) for the ACL. #351

Closed aavolkoff closed 11 years ago

aavolkoff commented 11 years ago

I think that the RLS for the ACL is a must.

Code example:

$acl = new Phalcon\Acl\Adapter\Memory();
$acl->setDefaultAction(Phalcon\Acl::DENY);
$acl->addRole('users');
/* Add RLS */
$rls = array('model' => 'robots',
'allowRead' => array('name' => 'C3PO'), 
'allowWrite' => array('name' => 'DarthVader'));
$acl->addRLS('users', $rls); // Now the user with "users" role can read only robot C3PO and can read and edit Darth Vader. There must be an option, when the value of the parameter can be obtained from the another model or table.
$acl->addRLS('users', array('model' => 'robotparts', 'property' => 'robot_id', 'rls' => $rls)) // Now, the user with "users" role can't read any 'robotsparts' of all robots, except C3PO and Darth Vader.
// And now:
$robot = Robots::findFirst('name = C3PO');
$robot->save() // returns "false", because user can only read C3PO data, but not write
foreach ($robots->getRobotsParts() as $part)
 $part->save() // returns "false"

There may be another functions, such as:

$acl->addRLS('users', $rls, true); // Now, all models from the application, where presents binding with "Robots" model, uses the same access restrictions as "Robots" model. It is very useful feature from ERP-practice.
/* Obtaining restrictions from the anoter model*/
$rls = array("model" => 'robots', 'allowRead' => array('model' => 'rowlevelsec', 'rolename' =>'users', 'modelAlias' => 'allowedRobots'));
$rls = array("model" => 'robots', 'allowRead' => array('model' => 'rowlevelsec', 'rolename' => true, 'modelAlias' => 'allowedRobots')); // The same as above, but 'rolename' parameter sets implicitly (later this parameter must be obtained from the role of the acl) .
$acl->('users', $rls); // Now the user with "users" role can read only robots that presents in "rowlevelsec" model in the objects that 'rolename' parameter match 'users', and 'allowedRobots' contains accessable robots. allowedRobots is alias for the belongsTo() method for "rowlevelsec" model.
phalcon commented 11 years ago

You can implement this by adding specific callbacks in your models:

<?php

class Robots extends Phalcon\Mvc\Model
{
    public function beforeCreate()
    {
        //Check permissions here if the current user doesn't have permissions to 
        //create return false
        $hasPermission = //...
        if ($hasPermission) {
            return false;
        } 
        return true;
    }

    public function beforeUpdate()
    {
        //Check permissions here if the current user doesn't have permissions 
        //to update return false
        $hasPermission = //...
        if ($hasPermission) {
            return false;
        } 
        return true;
    }

    public function beforeDelete()
    {
        //Check permissions here if the current user doesn't have permissions 
        //to delete return false
        $hasPermission = //...
        if ($hasPermission) {
            return false;
        } 
        return true;
    }

}

You can reuse the login by using a trait in PHP 5.4:

trait ModelRLS {

    public function beforeCreate()
    {
        //...
    }

    public function beforeUpdate()
    {
        //...
    }

    public function beforeDelete()
    {
        //...
    }   

}

Or using a BaseClass in PHP 5.3:

class ModelRLS extends Phalcon\Mvc\Model {

    public function beforeCreate()
    {
        //...
    }

    public function beforeUpdate()
    {
        //...
    }

    public function beforeDelete()
    {
        //...
    }   

}
aavolkoff commented 11 years ago

Phalcon, thank you for the reply! Sure I did such things already.

But in my previous post I just wanted to tell, that main feature of the RLS is that it works directly in the database.

Advantages:

  1. If the restrictions are working, than we need to retrieve the less data from the database. (traffic improvement)
  2. The application does not transforms the restricted relational table rows into the objects (ORM). (speed improvement)

Now, to restrict the users access to some objects we need to pass PHQL queries to Phalcon (with "Where" statement) or parse and transform all queries in the callbacks of the PHQL?

I can tell you that in the ERP platforms RLS - is the slowest thing. So, if we will produce code in php for the RLS it will works very-very-very slowly.

phalcon commented 11 years ago

You can access a database inside a model callback without using models, if you want to avoid the ORM layer:

class Robots \Phalcon\Mvc\Model
{

    public function beforeCreate()
    {
        return MyRLS::getInstance()->hasPermission($this);
    }

}

Then you could implement a user component accessing directly the database component or a PDO connection if you want:

<?php

class MyRLS extends Phalcon\DI\Injectable
{
    private static $_instance = null;

    public static function getInstance()
    {
        if (!self::$_instance) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

    public function hasPermission($model)
    {       
        $sql = 'SELECT COUNT(*) FROM permissions WHERE ...';
        $hasPermission = $this->db->fetchOne($sql);
        return $hasPermission[0];
    }

}

Also, you can return a custom meta-data according to those fields that the current user are restricted avoiding that the ORM query unnecessary fields:

class Robots \Phalcon\Mvc\Model
{

    public function beforeCreate()
    {
        return MyRLS::getInstance()->hasPermission($this);
    }

}

The user-component returns a custom array (http://docs.phalconphp.com/en/latest/reference/models.html#manual-meta-data) with the attributes allowed to the user:

 <?php

class MyRLS extends Phalcon\DI\Injectable
{
    public function getModelMetaData()
    {

        if ($this->session->get('user') == 'Peter') {
            return array(
                //...               
            );
        }

        return array(
            //...               
        );
    }
}

Additionally caching the user permissions in a fast backend like APC, Redis, Memcached or any other NoSQL database could help you to reduce the need of continuously query the SQL database to check permissions.