CADUCADO: La información de este post apesta por lo vieja. Es posible que ya no sea válida con las versiones más recientes de CakePHP. Se mantiene público para vergüenza y escarnio del autor.
Autorización, permisos, privilegios... como quieras llamarle. Después de aclararme (algo) las ideas con respecto al problema de la autentificación (asegurarse en lo posible de que un usuario es quien dice ser) ahora me toca romperme los cuernos con un sistema de autorización.
CakePHP proporciona algunas herramientas, como el soporte para ACL, pero es una de las partes que al parecer está más inestable en la versión 1.2 y también de las que menos documentación hay. No es que sea especialmente complicado lo de manejar las ACL, pero la implementación concreta igual es un poco abstracta de más por lo que he podido ver echando un vistazo al API.
En algunos de los artículos que he leído al respecto de la autorización he visto ideas que me han llamado la atención. Un sistema de permisos o autorizaciones es algo que tienes que pensar bastante bien porque debería cumplir varios requisitos:
- Estar lo más aislado posible del código de la aplicación. Esto es, la aplicación no tiene que saber nada sobre el "significado" de un permiso, sino simplemente si en una situación dada un usuario puede o no hacer algo, con uno o varios objetos. Hay que buscar el equilibro entre un modelo que sea abstracto y a la vez tenga significado. Y dado que estamos trabajando con CakePHP un objetivo es que el uso del sistema sea lo menos "intrusivo" posible.
- Ser fácilmente escalable, o sea, que pueda manejar los permisos con soltura a medida que crece la cantidad de usuarios y objetos, y a la vez, que sea fácil actualizar la estructura de permisos según sea necesario.
- Granularidad fina: que pueda controlar qué usuario puede hacer qué acción sobre qué objeto.
- Que funcione. Ya, ya sé. Es de perogrullo, pero un sistema de autorizaciones no sólo tiene que permitir a un usuario hacer lo que se supone que puede hacer, sino también debe prohibirle hacer lo que no puede.
Seguro que se me quedan más cosas, pero éstos requerimientos ya llegan bien para ponerte a pensar.
Enfoques
Existen varios enfoques para diseñar sistemas de autorización. No soy experto en el tema, así que no puedo hablar en mucha profundidad. Hay tres sospechosos habituales:
- Sistemas "a la Unix", en los que los objetos tienen un propietario que define los permisos básicos (de lectura, de escritura y otros que se puedan definir) para sí mismo, para un grupo y para el resto del mundo.
- Sistemas basados en ACL (Listas de Control de Acceso). En estas listas, se definen relaciones entre los sujetos, organizados en grupos jerárquicos, y los objetos, de modo que podemos permitir o denegar acceso a un objeto para un grupo atribuyendo en cascada el mismo privilegio a todos los grupos y sujetos que incluye. Y, posteriormente, podríamos añadir reglas para "matizar" esos privilegios en cada caso necesario.
- Sistemas basados en Roles. Los roles son conjuntos de permisos o privilegios que podemos asociar a los usuarios de forma individual o grupal. Esto es, definimos un rol describiendo todo lo que puede hacer y luego asignamos roles a los usuarios.
Por supuesto, existen sistemas más evolucionados y más o menos complejos. No voy a meterme, necesito desarrollar uno que me permita un control razonable.
Un modelo conceptual
Dándole vueltas he comenzado diseñar un sistema que parece tener buena pinta. Intentaré explicarlo paso a paso. Es bastante dependiente de la forma particular de hacer las cosas en Cake y de momento es sólo teórico, pues no lo he puesto en código aún. Quedan unos cuantos flecos por resolver.
Permisos
Una característica de CakePHP es la estructura de las URL. En una aplicación Cake cualquier cosa que pueda hacer un usuario se puede caracterizar con una URL de la forma /controlador/accion/atributos. Aunque hay casos particulares, nos vale como punto de partida.
Por lo tanto, para definir lo que un usuario puede hacer o no en esa aplicación podríamos empezar estableciendo si puede o no ejecutar la acción indicada por esa URL. Esquemáticamente, podríamos definir permisos en la forma:
Permitir /controlador/accion/
Denegar /controlador/accion/
Esta idea la encontré en este artículo de ThinkingPHP:
A lightweight approach to ACL, Y alguna más también.
Por ejemplo, podríamos referirnos a tener acceso a todas las acciones de un controlador concreto mediante un sistema de comodines, o incluso de expresiones regulares:
Permitir /controlador/*/
Podríamos forzar un poco las cosas y ampliar el uso de esta simple estructura con los atributos y referirnos incluso si se puede aplicar la acción a un objeto determinado, pero a la larga eso nos lleva a algunos problemas, así que nos quedaremos ahí de momento.
Roles
Por supuesto, lo anterior no nos vale de mucho si no relacionamos los permisos con alguien que los pueda utilizar o no. Pero en lugar de hacerlo directamente vamos a definir roles, es decir, vamos a agrupar los permisos en perfiles de privilegios.
Los roles pueden tener asociados varios permisos y éstos pueden pertenecer a distintos roles. (o no, este es uno de los puntos que habría que matizar). Pero espero que captes la idea. Es decir, o bien
Roles hasMany Permisos, o bien,
Roles HABTM Permisos
Lo siguiente sería asociar los Roles a los Usuarios que los pueden desempeñar. Un mismo rol se puede asociar a varios usuarios, y un usuario puede tener distintos roles. O sea que:
Usuarios HABTM Roles
De modeo que ahora podemos saber qué permisos tienen un usuario viendo sus roles.
Ámbitos
Pero hay una cosa que no hemos definido. ¿Sobre qué objetos puede actuar un usuario o se puede ejercer un rol?
Una forma simple sería definir el permiso incluyendo en la url el parámetro que define el objeto sobre el que se realiza la acción (normalmente el id). Pero probablemente tropezaremos con un problema al tratar con acciones que implican varios objetos (como un listado de posts de un blog y cosas así). ¿Cómo podemos restringir la búsqueda de objetos para que sólo devuelva los que corresponden al sujeto?
Una posibilidad que se me ocurre es definir ámbitos sobre los que se ejerce un
rol
permiso
. Los ámbitos describen los objetos a los que tendríamos acceso ejerciendo ese
rol
permiso
. Se me ocurren los siguientes:
- Objetos propiedad del usuario
- Objetos propiedad del grupo o grupos al que pertenezca el usuario
- Cualquier objeto
- Objeto especificado por su id
- Objetos del usuario especificado por su id
- Objetos del grupo espeificado por su id
(podría haber otros, claro)
Esto implica que llevamos cuenta de la propiedad de los objetos creados. Volveré sobre eso más adelante, de momento nos basta con tener presente que el sistema puede saber qué objetos pertenecen a un usuario.
Definiendo roles
Para definir un rol nos bastaría con darle un nombre, especificar un ámbito y crear permisos asociados con ese rol. El rol de root podría quedar definido así (de manera informal, claro):
rol: root
permisos:
Accion: /*/*/
Permitir: Sí
ambito: Cualquier objeto
rol: root
Un rol de visitante no registrado a un blog podría definirse así para que pueda ver los posts y comentar en ellos:
rol: no registrado
permisos
Accion: /Posts/ver
ambito: objetos del grupo "cualquiera"
Permitir: Sí
rol: no registrado
permisos
Accion /Posts/comentar
ambito: objetos del grupo "cualquiera"
Permitir: Sí
rol: no registrado
Políticas
Un aspecto que nos queda pendiente es saber qué se hace cuando no hay una regla explícita para un permiso. Una forma de resolver esto puede ser estableciendo políticas que regulen la manera en que se asignen permisos por defecto. Por ejemplo, una política podría ser la de denegar cualquier acceso salvo que se indique lo contrario. En cierto modo, una política puede definirse como un permiso por defecto para una determinada acción o para un determinado patrón de acciones.
Propiedad de los objetos
Todo lo anterior nos permitiría saber qué puede hacer un usuario en nuestro sistema, pero no nos dice mucho acerca de sobre qué objetos puede actuar. Los ámbitos nos permiten describir el rango de objetos, pero con referencia a sus propietarios.
Un sistema de propiedad de los objtos es algo tan simple como asociar cada objeto con un usuario o un grupo. Esto se podría hacer en la tabla de cada modelo de objeto "apropiable" o en una tabla aparte en la que se defina el objeto (mediante su modelo y su identificador único) y el sujeto que lo posee (usuario o grupo).
Una de las cosas maravillosas de CakePHP 1.2 es que este proceso de asignar la propiedad de los objetos se podría "automatizar" mediante un behavior "apropiable" para usar en los modelos de objetos susceptibles de tener poseedor. El behavior podría utilizar los Callbacks adecuados para poblar los campos de propiedad con los datos del usuario actual y/o los grupos a los que pertenece.
Determinando los objetos accesibles
Con la información de ámbito obtenida al analizar los
roles
permisos
ejercidos por un usuario debería ser fácil establecer las condiciones que sirvan para filtrar los objetos a los que el usuario tiene acceso. Por ejemplo, el ámbito "Objetos propiedad del usuario" se puede traducir como "propietario_id = usuario.id" (creo que se me entiende).
Estas condiciones se pueden pasar a las acciones para que tenerlas en cuenta en la búsqueda de datos.
Esto nos llevaría a que el proceso de autorización tendría dos fases:
- Determinar si el usuario puede realizar o no la acción que solicita.
- En caso positivo, determinar sobre qué objetos puede realizarla, o bien si los objetos sobre los que solicita realizarla pertenecen al ámbito definido en el
rol
permiso
.
- En caso de que alguno de los dos puntos anteriores falle, comunicar al usuario que no tiene permisos y redirigirlo a un lugar adecuado.
Problemas del modelo conceptual
Hay algunos temas sin resolver en el modelo o que potencialmente pueden generar problemas.
El más importante tiene que ver con los conflictos de roles, esto es, si un usuario puede tener roles que definen permisos distintos o en distintos ámbitos para la misma acción. En particular si los permisos son contradictorios. Es necesario establecer un sistema que permita definir una precedencia.
Por ejemplo, un rol de usuario normal podría tener permisos para añadir, ver y editar sus propios registros pero no para borrarlos. Esto se podría definir de forma económica:
Permitir /registros/*
Denegar /registros/borrar
Teniendo un sistema de precedencia que determine correctamente que el segundo permiso sobreescribe al primeto. Aunque también podríamos definir el mismo rol, con más reglas:
Permitir /registros/agregar
Permitir /registros/ver
Permitir /registros/editar
Y una política que deniegue cualquier permiso no explícitado.
En fin. Lo dejo por ahora. Todavía tengo que darle un par de vueltas antes de poner esto en código. Es necesario también definir un API para interrogar al sistema, que adoptará la forma de un componente para añadir a los controladores que deban tener supervisión de permisos.
También hemos dicho hace un rato que hará falta un behavior que se encargue de registrar la propiedad de los objetos, así como tal vez algunos plugins que nos permitan manejar el sistema y definir reglas, roles y políticas.