Con html::tagValue ($campo) puedes acceder en la vista a los valores de los campos del modelo del que estás construyendo un formulario (son los que el Controller tenga en Controller::data).
O sea, cuando uno hace cosas como ésta:
$this->data = $this->Model->Read ()
puede acceder a esos datos mediante el método tagValue del HtmlHelper.
Muy útil para construir enlaces en la vista, sin tener que crear una variable de vista para guardarlos.
Diario de aprendizaje del framework CakePHP. Otras notas de desarrollo y diseño web, realizado sobre Mac.
sábado, 14 de julio de 2007
domingo, 8 de julio de 2007
Título de la página desde el controller
Conocía este consejillo de Armando Sosa, para definir el título de la página desde la vista. Pero buscando no sé qué en el API, encontré que si ajustas la variable 'title' en el controller para pasársela a la view, queda fijada como título de la página. O sea, en la acción deseada del controlador ponemos:
$this->set ('title', 'Lo que quieras que sea el título');
Actualización (8-7-07)
La parte "mala" es que $title no aparece como variable de la vista. Sin embargo, puedes usar $this->pageTitle en la vista para acceder a ella.
$this->set ('title', 'Lo que quieras que sea el título');
Actualización (8-7-07)
La parte "mala" es que $title no aparece como variable de la vista. Sin embargo, puedes usar $this->pageTitle en la vista para acceder a ella.
Diseño de Interfaz: barra de botones (2)
En el post anterior sobre este tema dejaba colgados un par de asuntos. Fundamentalmente, la solución al problema de tener que fijar una altura para el contenedor UL, a fin de que se muestre "envolviendo" los botones.
Por otro lado, tampoco está de más simplificar el ejemplo y reducirlo al mínimo necesario, de modo que los temas estéticos se resuelvan en cada caso particular.
El problema de la altura de un contenedor de elementos flotados
Veamos. Los elementos flotantes (que tienen propiedad float asignada) salen del flujo normal, por lo que se altera visualmente el tamaño o posición de sus elementos contenedores o de los que sigan el flujo normal a partir de ellos. Por esta razón, si en una lista (UL) todos los ítems (LI) son flotantes, la lista no obtiene altura, porque para el motor de dibujado no contiene elementos (que como tienen float, están fuera del flujo y no cuentan a la hora de calcular el tamaño del contenedor).
Un apaño es darle altura explícitamente, pero es problemático si no sabemos exactamente qué altura darle o si el usuario cambia el tamaño de la letra. Así que es una solución menos que óptima.
La solución que he adoptado finalmente ha sido meter todo el conjunto en una DIV, de modo que sea ésta la que se encargue de simular el fondo y de contener visualmente la lista. En principio, va a ocurrir lo mismo, es decir, no va a obtener altura porque el contenido de la lista sigue "flotando". Pero existe una solución sencilla:
Basta con añadir un BR después de la UL al que asignamos clear:both;. De este modo, el BR fuerza a la DIV a "cerrarse" abarcando el contenido aunque esté flotante. El resultado es válido para XHTML, HTML y CSS, o sea, que bien.
El código mostrado a continuación incluye la solución con los estilos simplificados.
Por otro lado, tampoco está de más simplificar el ejemplo y reducirlo al mínimo necesario, de modo que los temas estéticos se resuelvan en cada caso particular.
El problema de la altura de un contenedor de elementos flotados
Veamos. Los elementos flotantes (que tienen propiedad float asignada) salen del flujo normal, por lo que se altera visualmente el tamaño o posición de sus elementos contenedores o de los que sigan el flujo normal a partir de ellos. Por esta razón, si en una lista (UL) todos los ítems (LI) son flotantes, la lista no obtiene altura, porque para el motor de dibujado no contiene elementos (que como tienen float, están fuera del flujo y no cuentan a la hora de calcular el tamaño del contenedor).
Un apaño es darle altura explícitamente, pero es problemático si no sabemos exactamente qué altura darle o si el usuario cambia el tamaño de la letra. Así que es una solución menos que óptima.
La solución que he adoptado finalmente ha sido meter todo el conjunto en una DIV, de modo que sea ésta la que se encargue de simular el fondo y de contener visualmente la lista. En principio, va a ocurrir lo mismo, es decir, no va a obtener altura porque el contenido de la lista sigue "flotando". Pero existe una solución sencilla:
Basta con añadir un BR después de la UL al que asignamos clear:both;. De este modo, el BR fuerza a la DIV a "cerrarse" abarcando el contenido aunque esté flotante. El resultado es válido para XHTML, HTML y CSS, o sea, que bien.
El código mostrado a continuación incluye la solución con los estilos simplificados.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<title>Barra de botones para registros</title>
<style type="text/css" media="screen">
/* Se eliminan los asuntos cosméticos y quedan sólo las reglas básicas para lograr la funcionalidad deseada */
div.barra_botones_registro {
display: block;
}
div.barra_botones_registro ul {
margin: 0;
padding: 0;
list-style: none;
}
div.barra_botones_registro ul li {
}
div.barra_botones_registro ul li a {
/* En esta regla se pondrían las características visuales de los botones */
display: block;
border: 1px solid #000;
padding: 2px 4px;
}
div.barra_botones_registro ul li.bloque1 {
float: left;
margin-right: 5px;
margin-left: 0px;
}
div.barra_botones_registro ul li.bloque2 {
float: left;
margin-right: 5px;
margin-left: 0px;
}
div.barra_botones_registro ul li.bloque3 {
float: right;
margin-right: 0;
margin-left: 5px;
}
div.barra_botones_registro ul li.bloque2.primer_boton_bloque2 {
margin-left: 10px;
}
</style>
</head>
<body>
<h1>Barra de botones para registro</h1>
<p>Ejemplo de una barra de botones para un registro. Está pensada para incluir tres grupos de botones, de forma que: </p>
<ul>
<li>Exista una separación lógica y visual</li>
<li>Se puedan prevenir pulsaciones erróneas</li>
</ul>
<div class="barra_botones_registro">
<ul>
<li class="bloque1"><a href="#">Borrar</a></li>
<li class="bloque1"><a href="#">Limpiar formulario</a></li>
<li class="bloque2 primer_boton_bloque2"><a href="#">Lista</a></li>
<li class="bloque2"><a href="http://some-site.com/">Imprimir</a></li>
<li class="bloque3"><a href="http://some-site.com/">Nuevo</a></li>
<li class="bloque3"><a href="http://some-site.com/">Duplicar</a></li>
</ul>
<br style="clear: both;" />
</div>
</body>
</html>
Diseño de interfaz: barra de botones para un registro
Lo mismo resulta que el título no es muy claro, lo que pretendo con esta nota es mostrar una prueba de concepto (qué bien suena eso) sobre cómo podría ser una barra de botones para operaciones con un registro de una tabla de base de datos.
No forma parte, estrictamente, del diseño del formulario, sino que más bien es un añadido en el que damos al usuario los medios para realizar algunas operaciones como volver a la lista de registros, borrar el objeto, crear uno nuevo, etc.
El problema que se trata de resolver es cómo agrupar los botones de una forma lógica, de forma que tengamos algunos de ellos fuera de la ruta de "escaneado visual" para evitar pulsaciones accidentales y que tengamos separados visualmente los bloques de botones.
La idea es que botones para acciones como crear un nuevo registro o editar el que se está viendo aparezcan en la parte inferior derecha, en el lugar donde termina el "escaneado de página", al menos para los lectores occidentales (aunque los principios creo que se pueden aplicar igual en sistemas de escritura con orientación diferente).
Los botones para eliminar registros o similares aparecerían en la esquina inferior izquierda, fuera de la ruta principal, de forma que normalmente el usuario tendría que moverse a propósito para buscarlos y pulsarlos.
En el centro, se pondrían otros botones. Algo más o menos como esta imagen que pego a continuación:

Usando HTML + CSS es posible acercarse bastante a esto. Básicamente se trata de que la barra de botones se asimila a una lista en la que cada ítem es un botón.
Para lograr que los diferentes botones se agrupen, a cada uno se le aplica una regla que lo identifica como propio de un grupo. De este modo, se pueden dar formatos diferentes a los botones.
El último problema sería definir puntos especiales en los puntos de separación de los botones.
Lo que he hecho ha sido empezar con la siguiente lista:
Si no se especifica ninguna regla de formato, la barra se ve como una lista normal. A todos los ítems se les aplican al menos una regla, que define a qué grupo pertenece. En un caso, se asigna una regla especial para indicar que se trata de un botón que inicia un bloque.
A continuación, iré introduciendo las reglas css.
La lista como barra
Esta regla aplica un estilo a la lista, básicamente para se pueda ver como una barra horizontal que abarca a los demás botones. El punto más "controvertido" es asignar un height. El problema es que con CSS estándar, al estar el contenido "flotado" UL no toma "altura". Una solución sería meter todo en una DIV y que sea ésta la que se encargue de mostrar el "fondo" de la barra, dejando la UL "invisible". Sin embargo, de momento lo voy a dejar así porque me parece que el concepto queda más claro. Aparte de eso, la mayor parte de las reglas son de carácter cosmético.
Pintando botones
El punto más importante aquí es el display: block de los botones. Luego los haremos "flotar" para colocarlos en el sitio adecuado mientras nos dejan el diseño fluido. Lo demás tiene que ver con la cosmética.
Colocando los botones
Las siguientes tres reglas definen el comportamiento de los grupos de botones. En el planteamiento que esto haciendo, los dos primeros grupos de botones se colocan hacia la izquierda y el tercero hacia la derecha, lo que sugiere el "float" correspondiente.
En el caso del bloque3, hay que tener en cuenta que el botón que queramos tener más a la derecha ha de ser el primero en la lista.
Las propiedades margin-right y margin-left nos permiten fijar la separación entre los botones. Debería ser igual en todos los bloques (excepto en el primer botón del bloque2, que trataremos en el siguiente punto y que debería separarse más).
De momento, hemos conseguido una separación flexible entre los botones del bloque2 y bloque3.
Separando el segundo bloque
Simplemente tenemos que identificar el primer botón de ese bloque y especificar que debe tener una separación mayor con respecto al bloque anterior.
Más allá
Una cosa que podríamos hacer es dar diferente formato visual a los bloques de botones para destacar algunos de ellos, los que tienen la "función primaria" frente a los que tienen funciones secundarias.
Ya que éste es un blog sobre aprendizaje de CakePHP. mencionar que probablemente escriba un helper de forma que sea fácil crear barras de botones que sigan este modelo. Al menos, una vez que consiga resolver el problema del fondo de la barra.
Una nota sobre las reglas
Como puedes ver, mis selectores css describen toda la ruta de la regla en algunos casos. Aparte de mejorar algo la comprensión de la regla (siempre sabes dónde se aplica), es una buena forma de asegurarse de que la regla se aplica sólo donde quieres que se aplique, definiendo su contexto.
No forma parte, estrictamente, del diseño del formulario, sino que más bien es un añadido en el que damos al usuario los medios para realizar algunas operaciones como volver a la lista de registros, borrar el objeto, crear uno nuevo, etc.
El problema que se trata de resolver es cómo agrupar los botones de una forma lógica, de forma que tengamos algunos de ellos fuera de la ruta de "escaneado visual" para evitar pulsaciones accidentales y que tengamos separados visualmente los bloques de botones.
La idea es que botones para acciones como crear un nuevo registro o editar el que se está viendo aparezcan en la parte inferior derecha, en el lugar donde termina el "escaneado de página", al menos para los lectores occidentales (aunque los principios creo que se pueden aplicar igual en sistemas de escritura con orientación diferente).
Los botones para eliminar registros o similares aparecerían en la esquina inferior izquierda, fuera de la ruta principal, de forma que normalmente el usuario tendría que moverse a propósito para buscarlos y pulsarlos.
En el centro, se pondrían otros botones. Algo más o menos como esta imagen que pego a continuación:

Usando HTML + CSS es posible acercarse bastante a esto. Básicamente se trata de que la barra de botones se asimila a una lista en la que cada ítem es un botón.
Para lograr que los diferentes botones se agrupen, a cada uno se le aplica una regla que lo identifica como propio de un grupo. De este modo, se pueden dar formatos diferentes a los botones.
El último problema sería definir puntos especiales en los puntos de separación de los botones.
Lo que he hecho ha sido empezar con la siguiente lista:
<ul class="barra_botones_registro">
<li class="bloque1">Borrar</li>
<li class="bloque1">Limpiar formulario</li>
<li class="bloque2 primer_boton_bloque2">Lista</li>
<li class="bloque2">Imprimir</li>
<li class="bloque3">Nuevo</li>
<li class="bloque3">Duplicar</li>
</ul>
Si no se especifica ninguna regla de formato, la barra se ve como una lista normal. A todos los ítems se les aplican al menos una regla, que define a qué grupo pertenece. En un caso, se asigna una regla especial para indicar que se trata de un botón que inicia un bloque.
A continuación, iré introduciendo las reglas css.
La lista como barra
Esta regla aplica un estilo a la lista, básicamente para se pueda ver como una barra horizontal que abarca a los demás botones. El punto más "controvertido" es asignar un height. El problema es que con CSS estándar, al estar el contenido "flotado" UL no toma "altura". Una solución sería meter todo en una DIV y que sea ésta la que se encargue de mostrar el "fondo" de la barra, dejando la UL "invisible". Sin embargo, de momento lo voy a dejar así porque me parece que el concepto queda más claro. Aparte de eso, la mayor parte de las reglas son de carácter cosmético.
ul.barra_botones_registro {
display: block;
margin: 0;
padding: 2px 4px;
border: 1px solid black;
background-color: #E1B250;
height: 20px;
}
Pintando botones
El punto más importante aquí es el display: block de los botones. Luego los haremos "flotar" para colocarlos en el sitio adecuado mientras nos dejan el diseño fluido. Lo demás tiene que ver con la cosmética.
ul.barra_botones_registro li {
display: block;
padding: 2px 6px;
border: 2px outset #907130;
font-family: Verdana, "MS Trebuchet", sans-serif;
font-size: 10px;
background-color: #FC6;
}
Colocando los botones
Las siguientes tres reglas definen el comportamiento de los grupos de botones. En el planteamiento que esto haciendo, los dos primeros grupos de botones se colocan hacia la izquierda y el tercero hacia la derecha, lo que sugiere el "float" correspondiente.
En el caso del bloque3, hay que tener en cuenta que el botón que queramos tener más a la derecha ha de ser el primero en la lista.
Las propiedades margin-right y margin-left nos permiten fijar la separación entre los botones. Debería ser igual en todos los bloques (excepto en el primer botón del bloque2, que trataremos en el siguiente punto y que debería separarse más).
De momento, hemos conseguido una separación flexible entre los botones del bloque2 y bloque3.
ul.barra_botones_registro li.bloque1, ul.barra_botones_registro li.bloque2 {
float: left;
margin-right: 5px;
margin-left: 0px;
}
ul.barra_botones_registro li.bloque3 {
float: right;
margin-right: 0;
margin-left: 5px;
}
Separando el segundo bloque
Simplemente tenemos que identificar el primer botón de ese bloque y especificar que debe tener una separación mayor con respecto al bloque anterior.
.primer_boton_bloque2 {
margin-left: 10px;
}
Más allá
Una cosa que podríamos hacer es dar diferente formato visual a los bloques de botones para destacar algunos de ellos, los que tienen la "función primaria" frente a los que tienen funciones secundarias.
Ya que éste es un blog sobre aprendizaje de CakePHP. mencionar que probablemente escriba un helper de forma que sea fácil crear barras de botones que sigan este modelo. Al menos, una vez que consiga resolver el problema del fondo de la barra.
Una nota sobre las reglas
Como puedes ver, mis selectores css describen toda la ruta de la regla en algunos casos. Aparte de mejorar algo la comprensión de la regla (siempre sabes dónde se aplica), es una buena forma de asegurarse de que la regla se aplica sólo donde quieres que se aplique, definiendo su contexto.
miércoles, 4 de julio de 2007
Comprobar que algo es único
Tenía la impresión de haber escrito algo sobre esto, pero parece que no. Así que pego este pequeño método que comprueba si un modelo es único. En este caso, un usuario comprobando si su nombre es único.
La clase Model tiene un método isUnique () al que le podemos pasar varias condiciones, en formato de pares "campo" => "condición" (como los métodos Find) y un segundo parámetro para booleano para responder en cuanto una condición se cumpla. Si le pasas true, isUnique devuelve true con tal de que se cumple alguna de las condiciones. Si le pasas false, tienen que cumplirse todas las condiciones.
Un pequeño falloUna pequeña limitación de esta función es que no está pendiente automáticamente del id del modelo. Esto genera el problema de que cuando editas un modelo, la validación falla porque efectivamente existe un registro igual en la base de datos: justamente el que estás editando. Incluyendo el chequeo del id como condición en caso de que estemos editando un registro, funcionará correctamente. Para saber si un modelo está en "edición" no tenemos más que mirar si hay valor en model::id.
Cuando estamos añadiendo un modelo (todavía no está guardado en la base de datos) el problema no se presenta y valida correctamente.
Pienso que el código se explica bastante bien:
La clase Model tiene un método isUnique () al que le podemos pasar varias condiciones, en formato de pares "campo" => "condición" (como los métodos Find) y un segundo parámetro para booleano para responder en cuanto una condición se cumpla. Si le pasas true, isUnique devuelve true con tal de que se cumple alguna de las condiciones. Si le pasas false, tienen que cumplirse todas las condiciones.
Cuando estamos añadiendo un modelo (todavía no está guardado en la base de datos) el problema no se presenta y valida correctamente.
Pienso que el código se explica bastante bien:
function usuarioEsUnico ($nombreUsuario) {
// Empezamos suponiendo que el nombre no va a validar
$esValido = false;
// La condición básica es que no coincidan los nombres de usuario
$condiciones['usuario'] = $nombreUsuario;
// Si estamos editando (hay valor en model::id, entonces comprobamos que no sea el mismo id
if ($this->id) {
$condiciones['id'] = '<> '.$this->id;
}
// Y ahora preguntamos al modelo si es único
$esValido = $this->isUnique ($condiciones, false);
return $esValido;
}
miércoles, 27 de junio de 2007
Acerca de la autorización (CADUCADO)
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.
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.
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
. 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
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
rolpermiso
. - En caso de que alguno de los dos puntos anteriores falle, comunicar al usuario que no tiene permisos y redirigirlo a un lugar adecuado.
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.
lunes, 25 de junio de 2007
Acerca de la autentificación (CADUCADO)
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.
Después de varias semanas parece que va siendo hora de empezar a escribir código de verdad y no simples ejercicios y pruebas "a ver cómo va esto del Cake".
Por supuesto, para una aplicación seria tengo que buscar un sistema de autentificación y de gestión de permisos.
CakePHP 1.1 no incorpora ningún método propio de autentificación y sí un sistema de ACL (Access Control Lists) sobre la base de que:
- No es difícil desarrollarlo usando el framework
- Cada desarrollador tienen sus necesidades
Aparte, existen unos cuantos sistemas de autentificación que son interesantes o tienen elementos interesantes:
El problema es que ninguno de ellos me acaba de convencer totalmente por una u otra razón.
Por lo pronto, habría que separar el sistema de autentificación del sistema de permisos. O al menos eso es lo que quiero hacer yo, y en alguno de los casos señalados hay cierta interacción entre ambos.
Quizá lo mejor sea coger lo que más me guste de cada uno y hacer algún refrito.
Autentificación
Lidiar con la autentificación supone intentar que:
- Los usuarios del sistema con credenciales adecuadas puedan "entrar"
- Evitar que usuarios maliciosos puedan lograr acceso o hacerse con contraseñas legítimas
- Almacenar las contraseñas encriptadas para que no se puedan obtener en caso de acceso a la base de datos (cierto, si un atacante accede a la base de datos estás vendido de todos modos, pero Dieter se refiere, por ejemplo a los alojamientos compartidos, donde hay técnicos que podrían curiosear).
- Yo añadiría que la aplicación no permita ver las contraseñas (ni siquiera los hash) a ningún usuario, ni siquiera administrador ni el mismo propietario. Si la contraseña se pierde, se cambia por otra, no es tan difícil.
- Enviar las contraseñas encriptadas del navegador al servidor, bien sirviendo por https (http + ssl, de modo que toda la comunicación viaja encriptada) o bien encriptando con Javascript en el navegador.
- Para dificultarlo más, utilizar una "sal" que convierta cada contraseña en única para la sesión. Esto es, que exista una especie de modificación de la contraseña para una sesión en particular, de modo que incluso en el supuesto caso de que un atacante intercepte la contraseña encriptada no pueda utilizarla en otra sesión. La cosa es generar una cadena (la sal) al iniciar una nueva sesión, incorporarla en el formulario (o en una cookie) del lado el cliente y en la sesión del para que se añada de algún modo a la contraseña (por ejemplo, encriptar la contraseña en el formulario y adjuntarle la "sal" y a lo mejor encriptar de nuevo). En el servidor, hacer algo parecido con la contraseña almacenada y comparar los hash resultantes.
- Lidiar con los ataques de fuerza bruta ue prueban a gran velocidad miles de contraseñas hasta encontrar alguna válida. Una forma de resolver esto es limitando la cantidad de intentos que se pueden hacer en un tiempo determinado, estableciendo listas "negras" de IP bloqueadas.
- Lidiar con la SQL injection, o sea, un intento de colar código SQL en los datos del formulario de login con el que se pueden conseguir cosas tan hermosas como login sin contraseña ni nada. Simplemente, un saneamiento de datos al recibir un formulario lo previene.
- Yo añadiría establecer unas reglas de validación de las contraseñas que obliguen a los usuarios a elegir contraseñas sólidas, como por ejemplo, incluir caracteres en mayúscula, minúscula, números y algún signo de puntuación. Es molesto, pero más molesto es que te birlen las credenciales y hagan guarrindongadas con tu cuenta.
- Otra historia es prevenir el posible "secuestro de sesión", es decir, que un usuario malicioso nos "sustituya" en medio de una sesión en la que ya estamos autentificados. Se puede hacer algo al respecto si guardamos suficiente información sobre la sesión actual, de modo que podamos detectar cosas como que haya cambiado la IP del cliente en plena sesión y cosas así.
- Si no hay usuarios conectados, presentar el login (o si el usuario activa un enlace para que le salga el login)
- Mantener la información del usuario conectado para no pedirle el login cada dos por tres.
- Si un usuario está conectado, que no le salga el login.
- Si una acción está reservada a usuarios conectados, actuar en consecuencia. Es decir, que le pida el login si no está conectado ya o que no se lo pida si ya está dentro.
Debe haber un componente. Puesto que la acción se lleva a cabo en los controladores, es en éstos donde debemos introducir el código que se encarga de gestionar los usuarios conectados. La mejor forma es un componente.
Por otro lado, habría que definir modelos para usuario e intentos de login, (los grupos, en su momento). El modelo de usuario podría tener una información acerca del último login o del estado de conexión. O igual es mejor tenerlo en una tabla aparte. Algo así:
Users
id
username
password
login_desde (para controlar si agota el tiempo de sesión, hay que definir un tiempo de timeout)
remote_ip (para controlar si cambia en algún momento)
Intentos
id
remote_ip (para controlar la IP que está intentando acceder)
status (bloqueada, lista negra..., esto nos permitiría controlar que una determinada IP sea tratada de modo especial)
Tiene que haber, lógicamente controlador y modelos para administrar los usuarios y también alguna forma de revisar los intentos de conexión para poder hacer cosas como desconectar usuarios desde el sistem, o desbloquear/bloquear manualmente direcciones, mostrar información del usuario conectado.
Un helper para generar formularios de login con los scripts de encriptación tampoco vendría mal.
Sobre este sistema, se podría montar un sistema de perfiles que nos permita tener información y preferencias del usuario conectado, y de grupos para asignar permisos en bloque. Eso sí, el tema de permisos lo dejaré para otro rato. Por hoy ya me llega.
Alguna aclaración sobre este sitio
Antes de continuar "pasteleando" y en vista de que se empiezan a recibir visitas (gracias, de verdad), me gustaría dejar claros algunos asuntos.
Este sitio no pretende ser una referencia sobre CakePHP, sino más bien el diario de aprendizaje de un tipo que ha decidido portar sus aplicaciones web a este framework.
"Yo era un desgraciado..."
Después de varios años escribiendo aplicaciones PHP he acabado, como mucha gente, creando mi propio pseudo-framework, con resultados más o menos irregulares. Llegado un punto, el nivel de desestructuración y spaghetti alcanzó cotas poco manejables, lo que aconsejaba (más bien imponía) un cambio.
Decidido a empezar desde cero, comencé a tomar nota de las características y necesidades que tenía que cubrir el nuevo planteamiento. Mi idea era reescribir mi framework y me lo fui tomando con calma. Pero con el tiempo, me encontré con un cuaderno con un montón de notas que dejaban bastante claro por dónde quería tirar.
Llegué a la conclusión de que quería que mi framework se basase en MVC, que tuviese la suficiente inteligencia como para hacer algo parecido a ActiveRecord y scaffolding (aunque entonces no sabia ni que existía eso), que me permitiese más libertad con el HTML, que fuese más modular, más sencillo de escribir, que llevase bien las bases de datos relaciones, etc.
En algún momento, me hablaron de CakePHP y le eché un vistazo, pero aún no estaba yo lo bastante maduro como para caerme del guindo. Sin embargo, tomé nota de algunas de las ideas y las incorporé a mi lista.
Unas semanas después, una nueva conversación sobre el tema me decidió a mirar en serio CakePHP y encontré que todas las características (y más que ni sabia que necesitaba) estaban ya contempladas. Me leí el manual de cabo a rabo un par de veces, brujuleé por la Bakery y hasta eché un vistazo a un par de frameworks más.
Desde hace un mes y medio o así la decisión ya está tomada. Me siento cómodo con lo que CakePHP ofrece y a partir de ahora mis aplicaciones web van a migrar como está mandado.
El blog
El blog viene a cuento de lo anterior. No hay muchos recursos sobre CakePHP en castellano, y menos si has optado como es mi caso por la versión 1.2 (no tiene documentación oficial). Por otro lado, pensé que sería buena idea ir recogiendo aprendizajes, recursos y código en un sitio donde pudiese tenerlo todo a mano.
De ahí la idea del blog. Además, necesitaba la práctica de crearlo por otra razón y me venía bien trastear un sistema de blog más o menos popular (entre otras cosas porque aún es pronto para crear el mío encima de CakePHP...).
Por tanto, lo que se recoge aquí no es precisamente un manual, más bien es un diario de aprendizaje. El código, los ejemplos y las explicaciones pueden ser útiles, aunque si eres experto en Cake puede que sean más interesantes las aportaciones que puedas hacer en forma de comentarios que mis propios posts.
Acerca del código
El código que voy publicando puede no estar del todo completo pero he decido documentarlo lo más posible. Sobre todo por mí mismo, no veas el palo que resulta mirar una función que escribiste hace meses y ahora no saber ni qué parámetros se le pasan.
Suele pasar que cuando pongo un post aquí, se produce una reordenación de los bits en Internet relacionados con el tema y, de repente, aparecen varios artículos excelentes sobre el mismo, enseñando técnicas más simples, elegantes y eficaces, que el día anterior "no estaban" (más bien yo no supe encontrarlas). Por eso, a veces cambio mucho el código, incluso varias veces al día.
En fin, como puedes ver, se trata de un "blog de notas", más que otra cosa.
Este sitio no pretende ser una referencia sobre CakePHP, sino más bien el diario de aprendizaje de un tipo que ha decidido portar sus aplicaciones web a este framework.
"Yo era un desgraciado..."
Después de varios años escribiendo aplicaciones PHP he acabado, como mucha gente, creando mi propio pseudo-framework, con resultados más o menos irregulares. Llegado un punto, el nivel de desestructuración y spaghetti alcanzó cotas poco manejables, lo que aconsejaba (más bien imponía) un cambio.
Decidido a empezar desde cero, comencé a tomar nota de las características y necesidades que tenía que cubrir el nuevo planteamiento. Mi idea era reescribir mi framework y me lo fui tomando con calma. Pero con el tiempo, me encontré con un cuaderno con un montón de notas que dejaban bastante claro por dónde quería tirar.
Llegué a la conclusión de que quería que mi framework se basase en MVC, que tuviese la suficiente inteligencia como para hacer algo parecido a ActiveRecord y scaffolding (aunque entonces no sabia ni que existía eso), que me permitiese más libertad con el HTML, que fuese más modular, más sencillo de escribir, que llevase bien las bases de datos relaciones, etc.
En algún momento, me hablaron de CakePHP y le eché un vistazo, pero aún no estaba yo lo bastante maduro como para caerme del guindo. Sin embargo, tomé nota de algunas de las ideas y las incorporé a mi lista.
Unas semanas después, una nueva conversación sobre el tema me decidió a mirar en serio CakePHP y encontré que todas las características (y más que ni sabia que necesitaba) estaban ya contempladas. Me leí el manual de cabo a rabo un par de veces, brujuleé por la Bakery y hasta eché un vistazo a un par de frameworks más.
Desde hace un mes y medio o así la decisión ya está tomada. Me siento cómodo con lo que CakePHP ofrece y a partir de ahora mis aplicaciones web van a migrar como está mandado.
El blog
El blog viene a cuento de lo anterior. No hay muchos recursos sobre CakePHP en castellano, y menos si has optado como es mi caso por la versión 1.2 (no tiene documentación oficial). Por otro lado, pensé que sería buena idea ir recogiendo aprendizajes, recursos y código en un sitio donde pudiese tenerlo todo a mano.
De ahí la idea del blog. Además, necesitaba la práctica de crearlo por otra razón y me venía bien trastear un sistema de blog más o menos popular (entre otras cosas porque aún es pronto para crear el mío encima de CakePHP...).
Por tanto, lo que se recoge aquí no es precisamente un manual, más bien es un diario de aprendizaje. El código, los ejemplos y las explicaciones pueden ser útiles, aunque si eres experto en Cake puede que sean más interesantes las aportaciones que puedas hacer en forma de comentarios que mis propios posts.
Acerca del código
El código que voy publicando puede no estar del todo completo pero he decido documentarlo lo más posible. Sobre todo por mí mismo, no veas el palo que resulta mirar una función que escribiste hace meses y ahora no saber ni qué parámetros se le pasan.
Suele pasar que cuando pongo un post aquí, se produce una reordenación de los bits en Internet relacionados con el tema y, de repente, aparecen varios artículos excelentes sobre el mismo, enseñando técnicas más simples, elegantes y eficaces, que el día anterior "no estaban" (más bien yo no supe encontrarlas). Por eso, a veces cambio mucho el código, incluso varias veces al día.
En fin, como puedes ver, se trata de un "blog de notas", más que otra cosa.
lunes, 18 de junio de 2007
Manipulacion de arrays
Un artículo en ThinkingPHP sobre cómo se manipulan arrays usando la clase set, que resulta ser una herramienta estupenda para extraer información de arrays complejos (como los resultados de FindAll y así).
La clase Set tiene una variedad de métodos que voy a ponerme a explorar en cuanto pueda. Ya contaré.
La clase Set tiene una variedad de métodos que voy a ponerme a explorar en cuanto pueda. Ya contaré.
domingo, 17 de junio de 2007
Componente para mantener la información de paginado
Después de darle como muchas vueltas se me ha ocurrido escribir un componente para gestionar la información de paginado entre vistas. La idea es la siguiente:
Supongamos que estoy en una típica vista index que muestra una tabla de registros paginada y que me encuentro en la página "p" de la misma.
Entonces voy y pulso un botón o enlace para editar un registro cualquiera de esa página. Al terminar de editar ¿a dónde vuelvo? En principio a la vista de index (si lo he especificado así), pero ¿a qué página?.
Si no especifico la página de origen normalmente volveré a la página 1 y no a la "p" que es en la que estaba, lo cual es un engorro, por muchas razones que te puedes suponer.
El componente paging que he escrito básicamente permite a la acción index "acordarse" de qué página estaba mostrando la última vez.
Instrucciones de uso
Por si no queda claro, la cosa es así
Cómo funciona
Básicamente, el componente comprueba si hay información de paginación almacenada en la sesión y la lee si es así.
Seguidamente, comprueba si hay información en la URL. Esta información tiene prioridad porque Controller::paginate() la utiliza si está disponible para definir los límites de la búsqueda.
Finalmente, el componente combina la información procedente de todas las fuentes para determinar el nuevo contenido de la variable Controller::paginate y también lo guarda en la sesión.
La información se guarda de forma específica para cada controlador.
El código
He puesto el código en CakeBin, pero como no sé si durará, intento copiarlo aquí:
Supongamos que estoy en una típica vista index que muestra una tabla de registros paginada y que me encuentro en la página "p" de la misma.
Entonces voy y pulso un botón o enlace para editar un registro cualquiera de esa página. Al terminar de editar ¿a dónde vuelvo? En principio a la vista de index (si lo he especificado así), pero ¿a qué página?.
Si no especifico la página de origen normalmente volveré a la página 1 y no a la "p" que es en la que estaba, lo cual es un engorro, por muchas razones que te puedes suponer.
El componente paging que he escrito básicamente permite a la acción index "acordarse" de qué página estaba mostrando la última vez.
Instrucciones de uso
- Copiar el código al archivo /app/controllers/components/paging.php
- En el controller, añadir Paging a la lista de components
- En la acción en que queremos usarlo, llamar al método $this->Paging->manage()
- Opcionalmente puedes pasarle el nombre del modelo al que debe referirse la información de paginación
Por si no queda claro, la cosa es así
class MiControlador extends AppController {
var $components = array ('Paging');
function index () {
$this->Paging->manage();
}
}
Cómo funciona
Básicamente, el componente comprueba si hay información de paginación almacenada en la sesión y la lee si es así.
Seguidamente, comprueba si hay información en la URL. Esta información tiene prioridad porque Controller::paginate() la utiliza si está disponible para definir los límites de la búsqueda.
Finalmente, el componente combina la información procedente de todas las fuentes para determinar el nuevo contenido de la variable Controller::paginate y también lo guarda en la sesión.
La información se guarda de forma específica para cada controlador.
El código
He puesto el código en CakeBin, pero como no sé si durará, intento copiarlo aquí:
<?php
/**
* Component to manage paging info between reloads
*
* Created by Fran Iglesias on 2007-06-17.
* @version: 0.1
* Copyright (c) 2007 Fran Iglesias. All rights reserved.
**/
/**
* Usage:
*
* 1. Put paging.php into your /app/controllers/components folder
* 2. Add 'Paging' to your controller::components array
* 3. Call $this->Paging->manage () into the action that retrieves data using paginate()
*/
class PagingComponent extends Object {
var $components = array ('Session');
var $controller = false;
function startup (&$controller) {
$this->controller = $controller;
}
/**
* Manage pagination data retrieving and storing into session as needed,
* Paging info passed via URL arguments takes precedence over session data
*
* @params string $forModel Specify a model name to manages its paging data
* @return void
* @author Frankie
**/
function manage ($forModel = null) {
// Determines a model name to work
if (in_array ($forModel, $this->controller->modelNames)) {
$model = $forModel;
} else {
$model = $this->controller->modelNames[0];
}
// Check if there is paging info in the Session and read it
$sessionVarName = $this->controller->name . '_paging_info';
if ($this->Session->check ($sessionVarName)) {
$pagingInfoFromSession = $this->Session->read ($sessionVarName);
} else {
$pagingInfoFromSession = array ();
}
// Check if there is paging info in passedArgs and build a paginate-like record with it
$pagingArgs = $this->_getPagingFromArgs ();
// Set paging info based on paging info from session and form arguments
if (isset ($pagingInfoFromSession[$model])) {
$paging = am($pagingInfoFromSession[$model], $pagingArgs);
} else {
$paging = $pagingArgs;
}
// Pass the new paging info to controller->paginate variable
if (isset ($this->controller->paginate[$model])) {
$this->controller->paginate[$model] = am($this->controller->paginate[$model], $paging);
} else {
$this->controller->paginate[$model] = am ($this->controller->paginate, $paging);
}
// Store new paging info into Session
$this->Session->write ($sessionVarName, $this->controller->paginate);
return true;
}
/**
* Helper to read pagination info from URl if there is any
*
* @return void
* @author Frankie
**/
function _getPagingFromArgs () {
if (!isset ($this->controller->passedArgs)) {
return array ();
}
$paginate = array ();
if (isset ($this->controller->passedArgs['page'])) {
$paginate['page'] = $this->controller->passedArgs['page'];
}
if (isset ($this->controller->passedArgs['limit'])) {
$paginate['limit'] = $this->controller->passedArgs['limit'];
}
if (isset ($this->controller->passedArgs['sort']) && isset ($this->controller->passedArgs['direction'])) {
$paginate['order'][$this->controller->passedArgs['sort']] = $this->controller->passedArgs['direction'];
}
return $paginate;
}
}
?>
Suscribirse a:
Entradas (Atom)