Secciones

Listas de control de acceso (ACL)


Resumen

Phalcon\Acl proporciona una gestión fácil y ligera de las ACLs así como de los permisos adjuntos a ellas. Las Listas de Control de Acceso (ACL en inglés) permite a una aplicación controlar el acceso de las solicitudes a sus áreas y objetos subyacentes.

En resumen, ACLs tienen dos objetos: El objeto que necesita acceso, y el objeto al que necesitamos acceder. En el mundo de la programación, se denominan normalmente Roles y Componentes. En el mundo Phalcon, usamos la terminología Rol y Componente.

Caso de Uso

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

Rol - Acceso al Administrador - Acceso al Departamento de Contabilidad - Acceso al Administrador - Acceso al Invitado

Componente - Página de inicio de sesión - Página de administración - Página de facturas - Página de informes

Como se ha visto en el caso de uso anterior, un Rol se define como quién necesita acceder a un Componente particular, es decir un área de la aplicación. Un Componente se define como el área de la aplicación a la que se necesita acceder.

Usando el componente Phalcon\Acl, podemos vincular ambos, y reforzar la seguridad de nuestra aplicación, permitiendo sólo roles específicos estén vinculados a componentes específicos.

Activación

Phalcon\Acl usa adaptadores para almacenar y trabajar con roles y componentes. El único adaptador disponible por ahora es 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.

El constructor Phalcon\Acl toma como primer parámetro un adaptador usado para obtener la información relativa a la lista de control.

<?php

use Phalcon\Acl\Adapter\Memory;

$acl = new Memory();

La acción por defecto es Phalcon\Acl\Enum::DENY para cualquier Rol o Componente. 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

La clase Phalcon\Acl\Enum ofrece dos constantes que se pueden usar cuando se definen niveles de acceso.

  • 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

Como se ha mencionado anteriormente, un Phalcon\Acl\Role es un objeto que puede o no acceder a un conjunto de Componentes en la lista de acceso.

Hay dos maneras de añadir roles a nuestra lista. * usando un objeto Phalcon\Acl\Role o * usando una cadena, que representa el nombre del rol

Para ver esto en acción, usando el ejemplo descrito arriba, añadiremos los objetos Phalcon\Acl\Role relevantes a nuestra lista.

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

Un Componente es el área de la aplicación donde se controla el acceso. En una aplicación MVC, esto podría ser un Controlador. Aunque no es obligatorio, la clase Phalcon\Acl\Component se podría usar para definir los componentes de la aplicación. También es importante añadir acciones relativas a un componente para que la ACL pueda comprender qué debería controlar.

Hay dos maneras de añadir componentes a nuestra lista. * usando un objeto Phalcon\Acl\Component o * usando una cadena, que representa el nombre del componente

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. Como ya se ha mencionado anteriormente, la acción de acceso predeterminada para Phalcon\Acl es Phalcon\Acl\Enum::DENY, siguiendo el enfoque de lista blanca.

Para unir Roles y Componentes usamos los métodos allow() y deny() expuestos por la clase Phalcon\Acl\Memory.

<?php

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

$acl = new Memory();

/**
 * Add the roles
 */
$acl->addRole('manager');
$acl->addRole('accounting');
$acl->addRole('guest');


/**
 * Add the Components
 */

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

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

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

/**
 * Now tie them all together 
 */
$acl->allow('manager', 'admin', 'users');
$acl->allow('manager', 'reports', ['list', 'add']);
$acl->allow('*', 'session', '*');
$acl->allow('*', '*', 'view');

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

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. Para llevar esto a la perspectiva de una aplicación MVC, la línea anterior dice que al grupo manager se le permite el acceso al controlador admin y acción users.

$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. Otra vez llevando esta perspectiva a una aplicación MVC, la línea anterior dice que al grupo manager se permite el acceso al controlador reports y acciones list y add.

$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');

Del mismo modo, lo anterior da acceso para cualquier rol, a cualquier componente que tenga la acción view. En una aplicación MVC, lo anterior es equivalente a permitir a cualquier grupo acceder a cualquier controlador que expone un viewAction.

NOTA: Por favor hay que ser MUY cuidadoso al usar el comodín *. 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 es el framework de pruebas elegido por Phalcon, hay muchas pruebas en nuestro repositorio GitHub (carpeta tests) para ofrecer orientación e 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();

/**
 * Setup the ACL
 */
$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', 'users');
$acl->allow('manager', 'reports', ['list', 'add']);
$acl->allow('*', 'session', '*');
$acl->allow('*', '*', 'view');

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

// ....



// true - defined explicitly
$acl->isAllowed('manager', 'admin', 'dashboard');

// true - defined with wildcard
$acl->isAllowed('manager', 'session', 'login');

// true - defined with wildcard
$acl->isAllowed('accounting', 'reports', 'view');

// false - defined explicitly
$acl->isAllowed('guest', 'reports', 'view');

// false - default access level
$acl->isAllowed('guest', 'reports', 'add');

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();

/**
 * Setup the ACL
 */
$acl->addRole('manager');

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

// Set access level for role into components with custom function
$acl->allow(
    'manager',
    'admin',
    'dashboard',
    function ($name) {
        return boolval('Bob' !== $name);
    }
);

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();

/**
 * Setup the ACL
 */
$acl->addRole('manager');

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

// Set access level for role into components with custom function
$acl->allow(
    'manager',
    'admin',
    'dashboard',
    function ($name) {
        return boolval('Bob' !== $name);
    }
);

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

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

NOTA: El cuarto parámetro debe ser un vector. 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();

/**
 * Setup the ACL
 */
$acl->addRole('manager');

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

// Set access level for role into components with custom function
$acl->allow(
    'manager',
    'admin',
    'dashboard',
    function ($name) {
        return boolval('Bob' !== $name);
    }
);

// Returns false
$acl->isAllowed('manager', 'admin', 'dashboard');

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

// Returns true
$acl->isAllowed('manager', 'admin', 'dashboard');

Objetos Personalizados

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

Rol

Podemos implementar Phalcon\Acl\RoleAware en nuestra clase personalizada con su propia lógica. El siguiente ejemplo muestra un nuevo objeto rol llamado ManagerRole:

<?php

use Phalcon\Acl\RoleAware;

// Create our class which will be used as roleName
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;
    }

    // Implemented function from RoleAware Interface
    public function getRoleName()
    {
        return $this->roleName;
    }
}

Componente

Podemos implementar Phalcon\Acl\ComponentAware en nuestra clase personalizada con su propia lógica. El siguiente ejemplo muestra un nuevo objeto componente llamado ReportsComponent:

<?php

use Phalcon\Acl\ComponentAware;

// Create our class which will be used as componentName
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;
    }

    // Implemented function from ComponentAware Interface
    public function getComponentName()
    {
        return $this->componentName;
    }
}

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();

/**
 * Add the roles
 */
$acl->addRole('manager');

/**
 * Add the Components
 */
$acl->addComponent(
    'reports',
    [
        'list',
        'add',
        'view',
    ]
);

/**
 * Now tie them all together with a custom function. The ManagerRole and
 * ModelSbject parameters are necessary for the custom function to work 
 */
$acl->allow(
    'manager', 
    'reports', 
    'list',
    function (ManagerRole $manager, ModelComponent $model) {
        return boolval($manager->getId() === $model->getUserId());
    }
);

// Create the custom objects
$levelOne = new ManagerRole(1, 'manager-1');
$levelTwo = new ManagerRole(2, 'manager');
$admin    = new ManagerRole(3, 'manager');

// id - name - userId
$reports  = new ModelComponent(2, 'reports', 2);

// Check whether our user objects have access 
// Returns false
$acl->isAllowed($levelOne, $reports, 'list');

// Returns true
$acl->isAllowed($levelTwo, $reports, 'list');

// Returns false
$acl->isAllowed($admin, $reports, 'list');

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. Esto significa que puede definir un Phalcon\Acl\Role como base y después heredar de él ofreciendo acceso a superconjuntos o subconjuntos de componentes. 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();

/**
 * Create the roles
 */
$manager    = new Role('Managers');
$accounting = new Role('Accounting Department');
$guest      = new Role('Guests');

/**
 * Add the `guest` role to the ACL 
 */
$acl->addRole($guest);

/**
 * Add the `accounting` inheriting from `guest` 
 */
$acl->addRole($accounting, $guest);

/**
 * Add the `manager` inheriting from `accounting` 
 */
$acl->addRole($manager, $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();

/**
 * Create the roles
 */
$manager    = new Role('Managers');
$accounting = new Role('Accounting Department');
$guest      = new Role('Guests');

/**
 * Add all the roles
 */
$acl->addRole($manager);
$acl->addRole($accounting);
$acl->addRole($guest);

/**
 * Add the inheritance 
 */
$acl->addInherit($manager, $accounting);
$acl->addInherit($accounting, $guest);

Serialización

Phalcon\Acl se puede serializar y almacenar en un sistema de caché para mejorar la eficiencia. 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';
// Check whether ACL data already exist
if (true !== is_file($aclFile)) {

    // The ACL does not exist - build it
    $acl = new Memory();

    // ... Define roles, components, access, etc

    // Store serialized list into plain file
    file_put_contents(
        $aclFile,
        serialize($acl)
    );
} else {
    // Restore ACL object from serialized file
    $acl = unserialize(
        file_get_contents($aclFile)
    );
}

// Use ACL list as needed
if (true === $acl->isAllowed('manager', 'admin', 'dashboard')) {
    echo 'Access granted!';
} else {
    echo 'Access denied :(';
}

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 puede trabajar junto con el EventsManager si está presente, para disparar eventos a tu aplicación. 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;

// ...

// Create an event manager
$eventsManager = new Manager();

// Attach a listener for type 'acl'
$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();

// Setup the $acl
// ...

// Bind the eventsManager to the ACL component
$acl->setEventsManager($eventsManager);

Excepciones

Cualquier excepción lanzada en el espacio de nombres Phalcon\Acl será de tipo 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

Para poder crear sus propios adaptadores o extender los existentes debe implementar la interfaz Phalcon\Acl\AdapterInterface.