Secciones

Listas de control de acceso (ACL)


Resumen

Phalcon\Acl provides an easy and lightweight management of ACLs as well as the permissions attached to them. Access Control Lists (ACL) allow an application to control access to its areas and the underlying objects from requests.

En resumen, ACLs tienen dos objetos: El objeto que necesita acceso, y el objeto al que necesitamos acceder. In the programming world, these are usually referred to as Roles and Resources. In the Phalcon world, we use the terminology Role and Component.

Caso de Uso

Una aplicación contable necesita tener diferentes grupos de usuarios que tengan acceso a varias áreas de la aplicación.

Role - Administrator Access - Accounting Department Access - Manager Access - Guest Access

Component - Login page - Admin page - Invoices page - Reports page

As seen above in the use case, a Role is defined as who needs to access a particular Component i.e. an area of the application. A Component is defined as the area of the application that needs to be accessed.

Using the Phalcon\Acl component, we can tie those two together, and strengthen the security of our application, allowing only specific roles to be bound to specific components.

Activación

Phalcon\Acl uses adapters to store and work with roles and components. The only adapter available right now is Phalcon\Acl\Adapter\Memory. Si el adaptador usa la memoria, incrementa significativamente la velocidad en la que se accede a la ACL pero también presenta inconvenientes. El principal inconveniente es que la memoria no es persistente, con lo que el desarrollador necesita implementar una estrategia de almacenamiento de los datos de la ACL, para que no se genere la ACL en cada petición. Esto fácilmente puede suponer retrasos y procesamiento innecesario, especialmente si la ACL es bastante grande y/o se almacena en una base de datos o sistema de ficheros.

The Phalcon\Acl constructor takes as its first parameter an adapter used to retrieve the information related to the control list.

<?php

use Phalcon\Acl\Adapter\Memory;

$acl = new Memory();

The default action is Phalcon\Acl\Enum::DENY for any Role or Component. Esto tiene como propósito asegurar que sólo el desarrollador o la aplicación permiten el acceso a componentes específicos y no el propio componente ACL.

<?php

use Phalcon\Acl\Enum;
use Phalcon\Acl\Adapter\Memory;

$acl = new Memory();

$acl->setDefaultAction(Enum::ALLOW);

Constantes

The Phalcon\Acl\Enum class offers two constants that can be used when defining access levels.

  • Phalcon\Acl\Enum::ALLOW (1)
  • Phalcon\Acl\Enum::DENY (0 - predeterminado)

Puede usar estas constantes para definir los niveles de acceso para su ACL.

Añadir Roles

As mentioned above, a Phalcon\Acl\Role is an object that can or cannot access a set of Component in the access list.

Hay dos maneras de añadir roles a nuestra lista.

  • by using a Phalcon\Acl\Role object or
  • using a string, representing the name of the role

To see this in action, using the example outlined above, we will add the relevant Phalcon\Acl\Role objects in our list.

Objetos Rol. El primer parámetro es el nombre del rol, el segundo la descripción

<?php

use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Role;

$acl = new Memory();

$roleAdmins     = new Role('admins', 'Administrator Access');
$roleAccounting = new Role('accounting', 'Accounting Department Access'); 

$acl->addRole($roleAdmins);
$acl->addRole($roleAccounting);

Cadenas. Añade el rol justo con ese nombre directamente a la ACL:

<?php

use Phalcon\Acl\Adapter\Memory;

$acl = new Memory();

$acl->addRole('manager');
$acl->addRole('guest');

Añadir Componentes

A Component is the area of the application where access is controlled. In an MVC application, this would be a Controller. Although not mandatory, the Phalcon\Acl\Component class can be used to define components in the application. Also, it is important to add related actions to a component so that the ACL can understand what it should control.

Hay dos maneras de añadir componentes a nuestra lista.

Similar a addRole, addComponent requiere un nombre para el componente y una descripción opcional.

Objetos Componente. El primer parámetro es el nombre del componente, el segundo la descripción

<?php

use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Component;

$acl = new Memory();

$admin   = new Component('admin', 'Administration Pages');
$reports = new Component('reports', 'Reports Pages');

$acl->addComponent(
    $admin,
    [
        'dashboard',
        'users',
    ]
);

$acl->addComponent(
    $reports,
    [
        'list',
        'add',
    ]
);

Cadenas. Añade el componente justo con ese nombre directamente a la ACL:

<?php

use Phalcon\Acl\Adapter\Memory;

$acl = new Memory();

$acl->addComponent(
    'admin',
    [
        'dashboard',
        'users',
    ]
);

$acl->addComponent(
    'reports',
    [
        'list',
        'add',
    ]
);

Definición de Controles de Acceso

Después de definir los Roles y Componentes, necesitamos unirlos para poder crear la lista de acceso. Este es el paso más importante en la operación, ya que un pequeño error aquí puede permitir el acceso de los roles a componentes a los que el desarrollador no tenía intención de hacerlo. As mentioned earlier, the default access action for Phalcon\Acl is Phalcon\Acl\Enum::DENY, following the white list approach.

To tie Roles and Components together we use the allow() and deny() methods exposed by the Phalcon\Acl\Memory class.

<?php

use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Role;
use Phalcon\Acl\Component;

$acl = new Memory();

$acl->addRole('manager');
$acl->addRole('accounting');
$acl->addRole('guest');


$acl->addComponent(
    'admin',
    [
        'dashboard',
        'users',
        'view',
    ]
);

$acl->addComponent(
    'reports',
    [
        'list',
        'add',
        'view',
    ]
);

$acl->addComponent(
    'session',
    [
        'login',
        'logout',
    ]
);

$acl->allow('manager', 'admin', 'dashboard');
$acl->allow('manager', 'reports', ['list', 'add']);
$acl->allow('accounting', 'reports', '*');
$acl->allow('*', 'session', '*');

Lo que nos dicen las líneas anteriores:

$acl->allow('manager', 'admin', 'users');

Para el rol manager, se permite el acceso al componente admin y acción users. To bring this into perspective with an MVC application, the above line says that the group manager is allowed to access the admin controller and users action.

$acl->allow('manager', 'reports', ['list', 'add']);

También puede pasar un vector como parámetro action cuando se invoca al comando allow(). Lo anterior significa que para el rol manager, se permite el acceso al componente reports y las acciones list y add. Again to bring this into perspective with an MVC application, the above line says that the group manager is allowed to access the reports controller and list and add actions.

$acl->allow('*', 'session', '*');

También se pueden usar comodines para hacer un emparejamiento masivo para roles, componentes o acciones. En el ejemplo anterior, permitimos a todos los roles acceder a todas las acciones del componente session. Este comando dará acceso a los roles manager, accounting y guest, acceso al componente session y las acciones login y logout.

$acl->allow('*', '*', 'view');

Similarly, the above gives access to any role, any component that has the view action. In an MVC application, the above is the equivalent of allowing any group to access any controller that exposes a viewAction.

NOTE: Please be VERY careful when using the * wildcard. Es muy fácil cometer un error y el comodín, aunque parezca conveniente, puede permitir a los usuarios acceder a áreas de su aplicación a las que no debería. La mejor manera de estar 100% seguro es escribir pruebas específicamente para probar los permisos y la ACL. Estos se pueden hacer en el conjunto de pruebas unit instanciando el componente y luego comprobando si isAllowed() es true o false.

Codeception is the chosen testing framework for Phalcon and there are plenty of tests in our GitHub repository (tests folder) to offer guidance and ideas.

$acl->deny('guest', '*', 'view');

Para el rol guest, denegamos el acceso a todos los componentes con la acción view. A pesar de que el nivel de acceso por defecto es Acl\Enum::DENY en nuestro ejemplo anterior, hemos permitido específicamente la acción view para todos los roles y componentes. Esto incluye el rol guest. Queremos permitir que el rol guest acceda únicamente al componente session y las acciones login y logout, ya que guests no están autenticados en nuestra aplicación.

$acl->allow('*', '*', 'view');

Esto da acceso a view a todo el mundo, pero queremos que el rol guest sea excluido de éste, así que la siguiente línea hace lo que necesitamos.

$acl->deny('guest', '*', 'view');

Consultar

Una vez definida la lista, podemos consultarla para comprobar si un rol particular tiene acceso a un componente y acción particular. Para ello, necesitamos usar el método isAllowed().

<?php

use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Role;
use Phalcon\Acl\Component;

$acl = new Memory();

// #01
$acl->addRole('manager');
$acl->addRole('accounting');
$acl->addRole('guest');

// #02
$acl->addComponent(
    'admin',
    [
        'dashboard',
        'users',
        'view',
    ]
);

$acl->addComponent(
    'reports',
    [
        'list',
        'add',
        'view',
    ]
);

$acl->addComponent(
    'session',
    [
        'login',
        'logout',
    ]
);

// #03
$acl->allow('manager', 'admin', 'users');
$acl->allow('manager', 'reports', ['list', 'add']);
$acl->allow('*', 'session', '*');
$acl->allow('*', '*', 'view');

// #04
$acl->deny('guest', '*', 'view');

// ....

// #05
$acl->isAllowed('manager', 'admin', 'dashboard');

// #06
$acl->isAllowed('manager', 'session', 'login');

// #07
$acl->isAllowed('accounting', 'reports', 'view');

// #08
$acl->isAllowed('guest', 'reports', 'view');

// #09
$acl->isAllowed('guest', 'reports', 'add');

Legend

01: Add roles

02: Add components

03: Set up the allow list

04: Set up the deny list

05: true - defined explicitly

06: true - defined with wildcard

07: true - defined with wildcard

08: false - defined explicitly

09: false - default access level

Acceso Basado en Función

Dependiendo de las necesidades de su aplicación, podría necesitar otra capa de cálculos para permitir o denegar el acceso a los usuarios mediante la ACL. El método isAllowed() acepta un cuarto parámetro que es un callable como una función anónima.

Para aprovechar esta funcionalidad, necesita definir su función cuando llama el método allow() para el rol y componente que necesita. Supongamos que necesitamos permitir el acceso a todos los roles manager al componente admin excepto si su nombre es ‘Bob’ (¡Pobre Bob!). Para conseguir esto registraremos una función anónima que compruebe esta condición.

<?php

use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Role;
use Phalcon\Acl\Component;

$acl = new Memory();

// #01
$acl->addRole('manager');

// #02
$acl->addComponent(
    'admin',
    [
        'dashboard',
        'users',
        'view',
    ]
);

// #03
$acl->allow(
    'manager',
    'admin',
    'dashboard',
    function ($name) {
        return boolval('Bob' !== $name);
    }
);

Legend

01: Add roles

02: Add components

03: Set access level for role into components with custom function

Ahora que el callable está definido en la ACL, necesitaremos llamar al método isAllowed() con un vector como cuarto parámetro:

<?php

use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Role;
use Phalcon\Acl\Component;

$acl = new Memory();

// #01
$acl->addRole('manager');

// #02
$acl->addComponent(
    'admin',
    [
        'dashboard',
        'users',
        'view',
    ]
);

// #03
$acl->allow(
    'manager',
    'admin',
    'dashboard',
    function ($name) {
        return boolval('Bob' !== $name);
    }
);

// #04
$acl->isAllowed(
    'manager',
    'admin',
    'dashboard',
    [
        'name' => 'John',
    ]
);

// #05
$acl->isAllowed(
    'manager',
    'admin',
    'dashboard',
    [
        'name' => 'Bob',
    ]
);

Legend

01: Add roles

02: Add components

03: Set access level for role into components with custom function

04: Returns true

05: Returns false

NOTE:The fourth parameter must be an array. Cada elemento del vector representa un parámetro que acepta su función anónima. La clave del elemento es el nombre del parámetro, mientras que el valor es el que se pasará como valor del parámetro a la función.

También puede omitir el paso del cuarto parámetro a isAllowed() si lo desea. El valor por defecto para una llamada a isAllowed() sin el último parámetro es Acl\Enum::DENY. Para cambiar este comportamiento, puede hacer una llamada a setNoArgumentsDefaultAction():

<?php

use Phalcon\Acl\Enum;
use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Role;
use Phalcon\Acl\Component;

$acl = new Memory();

// #01
$acl->addRole('manager');

// #02
$acl->addComponent(
    'admin',
    [
        'dashboard',
        'users',
        'view',
    ]
);

// #03
$acl->allow(
    'manager',
    'admin',
    'dashboard',
    function ($name) {
        return boolval('Bob' !== $name);
    }
);

// #04
$acl->isAllowed('manager', 'admin', 'dashboard');

$acl->setNoArgumentsDefaultAction(
    Enum::ALLOW
);

// #05
$acl->isAllowed('manager', 'admin', 'dashboard');

Legend

01: Add roles

02: Add components

03: Set access level for role into components with custom function

04: Returns false

05: Returns true

Objetos Personalizados

Phalcon permite a los desarrolladores definir sus propios objetos rol y componente. Estos objetos deben implementar las interfaces facilitadas:

  • [Phalcon\Acl\RoleAwareInterface][acl-roleawareinterface] for Role
  • [Phalcon\Acl\ComponentAwareInterface][acl-componentawareinterface] for Component

Rol

We can implement the [Phalcon\Acl\RoleAwareInterface][acl-roleawareinterface] in our custom class with its own logic. El siguiente ejemplo muestra un nuevo objeto rol llamado ManagerRole:

<?php

use Phalcon\Acl\RoleAwareInterface;

// #01
class ManagerRole implements RoleAware
{
    protected $id;

    protected $roleName;

    public function __construct($id, $roleName)
    {
        $this->id       = $id;
        $this->roleName = $roleName;
    }

    public function getId()
    {
        return $this->id;
    }

    // #02
    public function getRoleName()
    {
        return $this->roleName;
    }
}

Legend

01: Create our class which will be used as roleName

02: Implemented function from RoleAware Interface

Componente

We can implement the Phalcon\Acl\ComponentAware in our custom class with its own logic. El siguiente ejemplo muestra un nuevo objeto componente llamado ReportsComponent:

<?php

use Phalcon\Acl\ComponentAware;

// #01
class ReportsComponent implements ComponentAware
{
    protected $id;

    protected $componentName;

    protected $userId;

    public function __construct($id, $componentName, $userId)
    {
        $this->id            = $id;
        $this->componentName = $componentName;
        $this->userId        = $userId;
    }

    public function getId()
    {
        return $this->id;
    }

    public function getUserId()
    {
        return $this->userId;
    }

    // #02
    public function getComponentName()
    {
        return $this->componentName;
    }
}

Legend

01: Create our class which will be used as componentName

02: Implemented function from ComponentAware Interface

ACL

Estos objetos ahora ya se pueden usar en nuestra ACL.

<?php

use ManagerRole;
use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Role;
use Phalcon\Acl\Component;
use ReportsComponent;

$acl = new Memory();

// #01
$acl->addRole('manager');

// #02
$acl->addComponent(
    'reports',
    [
        'list',
        'add',
        'view',
    ]
);

// #03
$acl->allow(
    'manager', 
    'reports', 
    'list',
    function (ManagerRole $manager, ModelComponent $model) {
        return boolval($manager->getId() === $model->getUserId());
    }
);

// #04
$levelOne = new ManagerRole(1, 'manager-1');
$levelTwo = new ManagerRole(2, 'manager');
$admin    = new ManagerRole(3, 'manager');

// #05
$reports  = new ModelComponent(2, 'reports', 2);

// #06
$acl->isAllowed($levelOne, $reports, 'list');

// #07
$acl->isAllowed($levelTwo, $reports, 'list');

// #08
$acl->isAllowed($admin, $reports, 'list');

Legend

01: Add roles

02: Add components

03: Now tie them all together with a custom function. The ManagerRole and ModelSubject parameters are necessary for the custom function to work

04: Create the custom objects

05: id - name - userId

06: Check whether our user objects have access. Returns false

07: Returns true

08: Returns false

La segunda llamada de $levelTwo evalúa true ya que getUserId() devuelve 2 que a su vez es evaluado en nuestra función personalizada. También tenga en cuenta que en la función personalizada allow() los objetos son automáticamente vinculados, proporcionando todos los datos necesarios para que la función personalizada funcione. La función personalizada puede aceptar cualquier número de parámetros adicionales. El orden de los parámetros definidos en el constructor function() no importa, porque los objetos serán descubiertos y vinculados automáticamente.

Herencia de Roles

Para eliminar la duplicación y aumentar la eficiencia en su aplicación, la ACL ofrece herencia de roles. This means that you can define one Phalcon\Acl\Role as a base and after that inherit from it offering access to supersets or subsets of components. Para utilizar la herencia de roles, necesita pasar el rol heredado como el segundo parámetro de la llamada del método, al añadir ese rol en la lista.

<?php

use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Role;

$acl = new Memory();

// #01
$manager    = new Role('Managers');
$accounting = new Role('Accounting Department');
$guest      = new Role('Guests');

// #02
$acl->addRole($guest);

// #03
$acl->addRole($accounting, $guest);

// #04
$acl->addRole($manager, $accounting);

Legend

01: Create roles

02: Add the guest role to the ACL

03: Add the accounting inheriting from guest

04: Add the manager inheriting from accounting

Sea cual sea el acceso que tenga guests, se propagará a accounting y a su vez accounting se propagará a manager. También puede pasar un vector de roles como segundo parámetro de addRole ofreciendo más flexibilidad.

Relaciones de Roles

Basado en el diseño de aplicación, podría preferir añadir primero todos los roles y después definir la relación entre ellos.

<?php

use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Role;

$acl = new Memory();

// #01
$manager    = new Role('Managers');
$accounting = new Role('Accounting Department');
$guest      = new Role('Guests');

// #02
$acl->addRole($manager);
$acl->addRole($accounting);
$acl->addRole($guest);

// #03
$acl->addInherit($manager, $accounting);
$acl->addInherit($accounting, $guest);

Legend

01: Create roles

02: Add all the roles

03: Add the inheritance

Serialización

Phalcon\Acl can be serialized and stored in a cache system to improve efficiency. Puede almacenar el objeto serializado en APC, sesión, sistema de ficheros, base de datos, Redis, etc. De esta manera puede recuperar la ACL rápidamente sin tener que leer los datos subyacentes que crea la ACL, ni tendrá que calcular la ACL en cada petición.

<?php

use Phalcon\Acl\Adapter\Memory;

$aclFile = 'app/security/acl.cache';
// #01
if (true !== is_file($aclFile)) {

    // #02
    $acl = new Memory();

    // #03
    // ...

    // #04
    file_put_contents(
        $aclFile,
        serialize($acl)
    );
} else {
    // #05
    $acl = unserialize(
        file_get_contents($aclFile)
    );
}

// #06
if (true === $acl->isAllowed('manager', 'admin', 'dashboard')) {
    echo 'Access granted!';
} else {
    echo 'Access denied :(';
}

Legend

01: Check whether ACL data already exist

02: The ACL does not exist - build it

03: Define roles, components, access, etc.

04: Store serialized list into a plain file

05: Restore the ACL object from the serialized file

06: Use the ACL list as needed

Es una buena práctica no usar serialización de la ACL durante el desarrollo, para asegurarse que su ACL se reconstruye en cada petición, mientras que se usan otros adaptadores o medios de serializado y almacenamiento para la ACL en producción.

Eventos

Phalcon\Acl can work in conjunction with the Events Manager if present, to fire events to your application. Los eventos se disparan usando el tipo acl. Los eventos que devuelven false pueden detener el rol activo. Los siguientes eventos están disponibles:

Nombre de evento Disparado ¿Puede detener el rol?
afterCheckAccess Lanzado después de comprobar si un rol o componente tiene acceso No
beforeCheckAccess Lanzado antes de comprobar si un rol o componente tiene acceso Si

El siguiente ejemplo demuestra como adjuntar oyentes a la ACL:

<?php

use Phalcon\Acl\Adapter\Memory;
use Phalcon\Events\Event;
use Phalcon\Events\Manager;

// ...

// #01
$eventsManager = new Manager();

// #02
$eventsManager->attach(
    'acl:beforeCheckAccess',
    function (Event $event, $acl) {
        echo $acl->getActiveRole() . PHP_EOL;

        echo $acl->getActiveComponent() . PHP_EOL;

        echo $acl->getActiveAccess() . PHP_EOL;
    }
);

$acl = new Memory();

// #03
// ...

// #04
$acl->setEventsManager($eventsManager);

Legend

01: Create an event manager

02: Attach a listener for type acl

03: Setup the $acl

04: Bind the eventsManager to the ACL component

Excepciones

Any exceptions thrown in the Phalcon\Acl namespace will be of type Phalcon\Acl\Exception. Puede usar esta excepción para capturar selectivamente sólo las excepciones lanzadas desde este componente.

<?php

use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Component;
use Phalcon\Acl\Exception;

try {
    $acl   = new Memory();
    $admin = new Component('*');
} catch (Exception $ex) {
    echo $ex->getMessage();
}

Personalizado

The Phalcon\Acl\AdapterInterface interface must be implemented in order to create your own ACL adapters or extend the existing ones.