ACL con Zend_Acl y MySQL. ¡Persiste tu lista de control de acceso en la BBDD y cárgala luego en la memoria!

Zend Framework logo

Hay aplicaciones web que distinguen diferentes roles o tipos de usuario para llevar a cabo determinadas tareas. Pensemos en el panel de control de una tienda virtual gestionada de forma distinta dependiendo de si se entra en el sistema con el rol comercial, el rol logístico o el rol administrador. La célebre tienda virtual Prestashop trabaja precisamente con estos roles. Por cierto, ya que mencionamos el tema de la tienda virtual, te animo a que eches un vistazo a este post para que te hagas una idea de cómo puede ser la base de datos de un sistema de e-commerce como éste, un sistema B2C (business to customer).

En un sistema B2C los comerciales pueden hacer cosas tales como dar de alta productos nuevos, gestionar las categorías, modificar el catálogo, etc.; los logísticos también pueden modificar el catálogo, pero tienen acceso a la información relativa al transporte y pueden ver estadísticas; finalmente, los administradores pueden hacer todo lo anterior, pero además pueden configurar la parte técnica del sistema.

Por lo tanto, vemos que en una aplicación así hay que separar los privilegios para que los de logística no puedan, por ejemplo, eliminar a otros usuarios del sistema, acción muy delicada que sólo puede ejecutar el administrador, o para que los comerciales no puedan ver la información estadística que sólo el personal de logística puede ver.

ACL (Access Control List) es una solución estándar al problema de la separación de privilegios. Con ACL se decide qué puede o no puede hacer un usuario ya autenticado en un sistema. Fíjate que una cosa es la autenticación y otra muy diferente el control del acceso. La autenticación es el proceso de decidir si un usuario es quien dice ser, mientras que el control de acceso es el proceso de decidir si un usuario ya autenticado puede acceder a un determinado recurso.

Zend_Acl es el componente de Zend Framework que implementa el control de acceso. Te recomiendo que leas la parte del manual de Zend que trata las listas de control de acceso para que vayas familiarizándote con esta tecnología. Para ello, pincha aquí, por favor. Concretamente, vas a ver que el problema se reduce a crear los roles que interactúan con el sistema, crear los objetos o recursos accesibles (scripts PHP, imágenes, programas JavaScript, etc.), y definir los permisos.

Sin más preámbulos, ¡vamos ya a implementar nuestro sistema ACL en una base de datos MySQL! La idea es almacenar la lista de control en la base de datos y construir su representación correspondiente en memoria cada vez que el servidor recibe una petición HTTP GET para servir un recurso. Como veremos un poco más adelante, inicializamos la ACL en una etapa temprana del flujo de ejecución del programa, en el Front Controller, o index.php.

Un SQL típico para persistir la ACL es éste:

CREATE TABLE IF NOT EXISTS roles (
    id tinyint UNSIGNED NOT NULL AUTO_INCREMENT,
    name VARCHAR(32) NOT NULL,
    PRIMARY KEY (id)
) ENGINE=InnoDB;
 
CREATE TABLE IF NOT EXISTS resources (
    id tinyint UNSIGNED NOT NULL AUTO_INCREMENT,
    name VARCHAR(64) NOT NULL,
    PRIMARY KEY (id)
) ENGINE=InnoDB;
 
CREATE TABLE IF NOT EXISTS roles_resources (
    id tinyint UNSIGNED NOT NULL AUTO_INCREMENT,
    id_role tinyint UNSIGNED NOT NULL,
    id_resource tinyint UNSIGNED NOT NULL,
    PRIMARY KEY (id),
    FOREIGN KEY (id_role) REFERENCES roles(id),
    FOREIGN KEY (id_resource) REFERENCES resources(id)
) ENGINE=InnoDB;
 
CREATE TABLE IF NOT EXISTS users (
    id tinyint UNSIGNED NOT NULL AUTO_INCREMENT,
    login VARCHAR(16) NOT NULL,
    password VARCHAR(40) NOT NULL,
    id_role tinyint UNSIGNED NOT NULL,
    PRIMARY KEY (id),
    FOREIGN KEY (id_role) REFERENCES roles(id)
) ENGINE=InnoDB;

A continuación hay que hacer un pequeño dump e insertar los roles, los recursos que maneja la aplicación, y rellenar la tabla de unión roles_resources para que quede constancia de quién puede hacer qué. Este proceso puede ser manual en aplicaciones pequeñas y medianas que no tengan muchos recursos, aunque es mejor automatizarlo. Por motivos didácticos nosotros hacemos a mano este proceso:

INSERT INTO roles(name) VALUES ('admin'), ('salesman'), ('logistic');
 
INSERT INTO resources(name) VALUES
 
# admin
 
('cpanel/admin/layout.php'),
('cpanel/admin/home.php'),
('cpanel/admin/users/delete.php'),
('cpanel/admin/users/edit.php'),
('cpanel/admin/users/index.php'),
('cpanel/admin/users/new.php'),
('cpanel/admin/users/index.php'),
 
# Here we dump more DATA...
 
# salesman
 
('cpanel/salesman/layout.php'),
('cpanel/salesman/home.php'),
('cpanel/salesman/categories/delete.php'),
('cpanel/salesman/categories/edit.php'),
('cpanel/salesman/categories/index.php'),
('cpanel/salesman/categories/new.php'),
('cpanel/salesman/products/index.php'),
('cpanel/salesman/products/new.php'),
('cpanel/salesman/products/edit.php'),
('cpanel/salesman/products/delete.php'),
('cpanel/salesman/products/index.php'),
 
# Here we dump more DATA...
 
# logistic
 
('cpanel/logistic/layout.php'),
('cpanel/logistic/home.php'),
('cpanel/logistic/stats/search.php'),
('cpanel/logistic/stats/index.php');
 
# Here we dump more DATA...
 
INSERT INTO roles_resources(id_role, id_resource) VALUES
 
# admin
 
(1,1),
(1,2),
(1,3),
(1,4),
(1,5),
(1,6),
(1,7),
 
# salesman
 
(2,8),
(2,9),
(2,10),
(2,11),
(2,12),
(2,13),
(2,14),
(2,15),
(2,16),
(2,17),
(2,18),
 
# logistic
 
(3,19),
(3,20),
(3,21),
(3,22);

Ahora hay que crear la función de carga del ACL en la memoria:

function initMySQLACL() {     
    $acl = new Zend_Acl();     
    $mysqli = dbConnect();     
    // Add roles     
    $sql = "SELECT * FROM roles";     
    $rsRoles = $mysqli->query($sql);     
    while ($row = $rsRoles->fetch_assoc()) $acl->addRole(new Zend_Acl_Role($row['name']));      
    // Add resources     
    $sql = "SELECT * FROM resources";     
    $rsResources = $mysqli->query($sql);     
    while ($row = $rsResources->fetch_assoc()) $acl->add(new Zend_Acl_Resource($row['name']));     
    // Add permissions     
    $sql = "SELECT ro.name as role_name, re.name as resource_name FROM roles as ro JOIN roles_resources as rr ON ro.id = rr.id_role JOIN resources as re ON re.id = rr.id_resource";     
    $rsRolesResources = $mysqli->query($sql);     
    while ($row = $rsRolesResources->fetch_assoc()) $acl->allow($row['role_name'], $row['resource_name']);     
    return $acl;     
}

Finalmente, dependiendo de la aplicación donde se implemente todo esto, sólo queda escribir algo parecido a lo siguiente en un estadio temprano del proceso de dispatching para inicializar nuestro sistema ACL basado en Zend Framework:

$acl = initMySQLACL(); 
$page = "{$_GET['page']}.php"; 
$users = new Users(); 
$role = $users->getRole($auth->getIdentity()); 
if ($acl->isAllowed($role, $page)) 
    require_once "cpanel/$role/layout.php"; 
else { 
    header('Location: http://' . BASE_URL . '/login');
    exit; 
}

Once again, hope to help!

Comments

  1. DooBie says

    Creo que hay un mini error de copy-paste. Donde haces la inserción de los roles_resources, asignas el mismo id_role a admin y salesman, y a logistic, se le asigna un id_role incorrecto.
    Deberia ser para admin id_role=1, salesman ir_role=2 y logistic id_role=3

    Buen tuto, me ha aclarado algunas cosas.
    Saludos!

    • admin says

      ¡Muchas gracias DooBie! Ya lo he corregido. Sucede que esa parte es precisamente la que debe automatizarse, hehe, por lo que en la práctica es bastante susceptible a errores. Imagínate, cada vez que hay un cambio en los recursos de la aplicación hay que actualizar esas inserciones…

  2. pya says

    Hola estoy empezando a usar Zend y tengo que hacer un lista de acceso me explicarias un poco mas de donde de poner cada cosas donde creo cada funcion por favorrrr

    • admin says

      ¡Hola pya! Pues verás, la función de inicialización de la ACL, initMySQLACL(), tienes que ponerla en un estadio temprano de la ejecución del programa, al principio, para que esté disponible en todo momento. ¡Nos interesa saber quién puede hacer qué desde el principio! Por tanto, initMySQLACL() tiene que estar en el index.php (el Front Controller) o en una clase especial para inicializar el entorno de la app.

  3. Robin says

    sabes he seguido los pasos como lo has planteado, bueno he ubicado la función initMySQLACL en el index.php y su llamada en el IndexController->IndexAction… pero al ejecutar la aplicación me genera este error: Message: Resource ‘.php’ not found. Otra cosa a que te refieres con cpanel?. Estaré esperando tu respuesta.

    • admin says

      cpanel quiere decir panel de control, es el url que se pone para acceder a tal o cual panel de administración de tal o cual rol. ¡Es bastante self-explanatory! ;-) Saludos

Deja un comentario

Tu dirección de correo electrónico no será publicada.