Autorización (Zend_Acl) y Autenticación (Zend_Auth)

Introducción

Este ejemplo es de un foro (¡que original!) uso Zend_Controller con estructura modular, también tengo un pequeño manual al respecto. A continuación la estructura de directorios.

application/		<--- dir de aplicaciones
    bootstrap.php
    default/		<--- modulo que se muestra de forma predeterminada
        controllers/
            IndexController.php <--- página de inicio
            PostController.php  <--- restringido a usuarios
        models/
        views/
    admin/		<--- modulo de administración
        controllers/
            IndexController.php
            ...
        models/
        views/
    auth/		<--- modulo del autenticación Zend_Auth
        controllers/
            IndexController.php
            loginController.php
            logoutController.php
            ...
        models/
        views/
html/			<--- directorio público, es decir el que se ve en internet
    index.php		<--- sólo tiene una línea que llama al bootstrap.php
library/          <--- Librerias, clases y esas cosas
    My/
        Acl.php           <--- lista de control de acceso
        AuthorizationPlugin.php  <--- plugin de autorización
    Zend/ <-- contiene las librerias de Zend Framework

En el módulo default se muestra la página predeterminada, es decir "el index" raiz, este tiene la página de inicio y post donde los usuarios podrán enviar sus comentarios y el acceso a este requiere autorización. El módulo admin será donde se administren los comentarios, ejemplo borrar, autorizar publicación, etc. todo este módulo requiere autorización. Por ultimo el módulo auth es donde se hace la autenticación y al igual que "el index" todos tiene acceso él, en este modulo estará un sistema de login y logout, por lo menos, pero yo ahí pongo la página para registrarse como nuevo usuario y recordar contraseñas. Pero intento mantener el ejemplo lo mas simple.

Autorización

Lista de control de acceso

Primero creamos una ACL en el archivo library/My/Acl.php

addRole(new Zend_Acl_Role('guest') );
		$this->addRole(new Zend_Acl_Role('user'), 'guest' );
		$this->addRole(new Zend_Acl_Role('admin'));

		// Recursos de lo general a lo particular
		$this->add(new Zend_Acl_Resource('default'));
		$this->add(new Zend_Acl_Resource('post'), 'default');
		$this->add(new Zend_Acl_Resource('auth'));
		$this->add(new Zend_Acl_Resource('admin'));

		// Asignar permisos
		// guest
		$this->allow('guest', array('default', 'auth') );
		$this->deny('guest', array('post', 'admin') );
		// user
		$this->allow('user', array('post') );
		// admin
		$this->allow('admin');
    }
}

Líneas 11 a 13 crea los roles que se le asignan a los usuarios; guest es el invitado, cualquier persona que entra a la página; user es el usuario registrado, hereda los permisos de guest y tiene permiso de entrar a recurso post (línea 26); por último el role admin tiene permiso de entrar a toda la página (línea 28).

Para asignar recursos la documentación de Zend_Acl recomienda ir de lo general a lo particular, es decir primero los modulos, luego los controladores y por último las acciones. Esto lo hago de las líneas 16 a 19 el caso particular es en la línea 17 pues quiero restringir el acceso al controlador post del modulo default.

Los permisos se asignan en las líneas 23 a 28. Con la particularidad de que en la línea 28 se le da permiso a admin de todo.

Authorization Plugin

Quiero hacer la autorización en cada click de cada página, para eso escribo un Controllers Plugins de Zend Framework en library/My/AuthorizationPlugin.php:

_auth = $auth;
        	$this->_acl = $acl;
	}
   
	public function preDispatch ( Zend_Controller_Request_Abstract $request )
	{
		// revisa que exista una identidad
		// obtengo la identidad y el "role" del usuario, sino tiene le pone 'guest'
		$role = $this->_auth->hasIdentity() ? $this->_auth->getInstance()->getIdentity()->role : 'guest';

		// toma el nombre del recurso actual
		if( $this->_acl->has( $this->getRequest()->getActionName() ) )
			$resource = $this->getRequest()->getActionName();
		elseif( $this->_acl->has( $this->getRequest()->getControllerName() ) )
			$resource = $this->getRequest()->getControllerName();
		elseif( $this->_acl->has( $this->getRequest()->getModuleName() ) )
			$resource = $this->getRequest()->getModuleName();
		
		// Si, la persona no pasa la prueba de autorización y su "role" es 'guest'
		// entonces no ha echo "login" y lo dirigo al controlador "login" del modulo "auth"
		if ( !$this->_acl->isAllowed($role, $resource) && $role == 'guest' )
		{
			$request->setModuleName('auth');
			$request->setControllerName('index');
		}
		// Ahora si la persona tiene un "role" distinto de 'guest' y aun así no pasa
		// la prueba de identificación lo mando a una página de error.
		elseif (!$this->_acl->isAllowed($role, $resource) )
		{
			$request->setModuleName('auth');
			$request->setControllerName('error');
		}

	}
}

Este plugin recibe un objeto de Zend_Auth (línea 13) y uno de Zend_Acl (línea 14) que es el definido arriba. En la línea 21 le pregunto al objeto Zend_Auth el role del usuario y las líneas 24 a 29 le pregunto a Zend_Acl si el nombre del modulo, controlador o acción estan en la ACL y si lo esta lo toma como recurso.

En la línea 33 le pregunto a Zend_Acl si el role tiene acceso al recurso, si falla y ademas el role es guest lo mando a la página de login. La línea 40 es un extra, pues si falla la autorización lo manda a una página de error.

Autenticación (Zend_Auth)

La siguiente sección esta basada en el artículo Tutorial: Getting Started with Zend_Auth

Primero en mi base de datos debe haber una tabla para usuarios con al menos las siguientes caracteristicas:

> Describe useres;
+-------+-------------------------------+------+-----+---------+----------------+
| Field | Type                          | Null | Key | Default | Extra          |
+-------+-------------------------------+------+-----+---------+----------------+
| id    | int(4) unsigned               | NO   | PRI | NULL    | auto_increment |
| lvl   | enum('user','editor','admin') | NO   |     | 0       |                |
| user  | varchar(60)                   | NO   | UNI |         |                |
| psw   | varchar(60)                   | NO   |     |         |                |
| name  | varchar(60)                   | NO   |     |         |                |
+-------+-------------------------------+------+-----+---------+----------------+

Uso MySQL pero no ha de ser muy distinto con otros servidores.

Modulo Autenticación

Antes que nada, la autenticación bien se puede programar toda en un controlador del modulo predeterminado, pero a mi me gusta separarlo del resto, porque conceptualmente hablando para mi debe ser independiente del sitio web.

Así empiezo por crear los archivos:

application/auth/controllers/IndexController.php

_redirect('/');
	}
}

application/auth/controllers/loginController.php

view->title = $this->view->translate->_("Login system");
		
		$config = new Zend_Config(require '../application/auth/configuration/authForm.php');
	
		$this->view->form = $form;
		
		if ( $this->_request->isPost() )
		{
			$data = $this->_request->getPost();
			if ( $form->isValid($data) )
			{
				// lee la configuración de la base de datos
				// iniciada en el bootstrap.php
				$db = Zend_Registry::get('db');
				
				// Le digo a Zend_Auth como leer la base de datos
				$authAdapter = new Zend_Auth_Adapter_DbTable($db);
				// selecciono la tabla
				$authAdapter->setTableName('users');
				// las identidades se leerán de la columna user
				$authAdapter->setIdentityColumn('user');
				// Las credenciales de la columna password
				$authAdapter->setCredentialColumn('password');
				
				// Ahora tomo los valores del formulario y los paso a Zend_Auth
				$authAdapter->setIdentity( $form->getValue('username') );
				$authAdapter->setCredential( md5( $form->getValue('password') ) );
				
				// Este paso no me queda claro XD pero es 
				// La ¡¡verdadera Autenticación!!
				$auth = Zend_Auth::getInstance();
				$result = $auth->authenticate($authAdapter);
				
				if ($result->isValid())
				{	// Si la autenticación es valida
					
					// Leo los valores guardado en $authAdapter y
					// los guardo en una variable de sesión
					$data = $authAdapter->getResultRowObject();
					$auth->getStorage()->write($data);
					$this->_redirect('/registration');
				} else {
					// En caso de que no sea valido
					// envío un mensaje de error.
					$this->view->message = 'Login or password incorrect.';
				}
			} // termina: if ( $form->isValid($data) )
		} // termina: if ( $this->_request->isPost() )
	} // termina: indexAction()
}

¡Paren de leer aquí! lo que sigue esta mal escrito e incompleto.

Editar el bootstrap.php

En algún lugar antes de iniciar el FrontController ponemos:

...
// toma los valores de Zend_Auth, aun no vemos esto no desesperes
$auth = Zend_Auth::getInstance();

// inicia nuestra lista de control de accesos
$acl = new My_Acl();
...
// setup FrontController
$front = Zend_Controller_Front::getInstance();
...

Luego despues de inicar el FrontController y configurarlo debemos registrar nuestro plugin (AuthPlugin.php) de la siguiente forma:

...
// setup FrontController
$front = Zend_Controller_Front::getInstance();
...// por lo general aqui va mas configuración de tipo $front->
$front->registerPlugin(new My_AuthPlugin($auth, $acl));
...

En este punto todo debe funcionar! pero.. sólo que el sitio no tiene ninguna forma de iniciar sesión (login) siendo que esto es el verdadero sistema de autenticación.

Zend_Auth y la verdadera autenticación

Esta es la página de login abajo esta el código, pero pueden encontrar una muy buena explicación en http://akrabat.com/zend-auth-tutorial/

_redirect('/index');
	}

	function loginAction()
	{
		// ¡Cielos! esta parte no la explico. pero es el formulario para login
		$config = new Zend_Config_Ini('../application/config/authForms.ini', 'login');
		$form = new Zend_Form($config->form);
		$form->setAction( $this->view->baseUrl() 
				. '/' . $this->getRequest()->getControllerName()
				. '/' . $this->getRequest()->getActionName() );

		$this->view->form = $form;

		
		// si ya se envio el formulario por medio de post
		if ( $this->_request->isPost() )
		{
			$data = $this->_request->getPost();

			// revisa que los datos sean validos y si los son
			if ( $form->isValid($data) )
			{
				$db = Zend_Registry::get('db'); // lee la configuracion de la base datos
				
				// ¡¡por fin!! usamos Zend_Auth
				// lo inciamos 
				$authAdapter = new Zend_Auth_Adapter_DbTable($db);
				$authAdapter->setTableName('users'); // selecciono la tabla
				$authAdapter->setIdentityColumn('user'); // columna de identidad
				$authAdapter->setCredentialColumn('psw'); // columna de credenciales
				
				// pone las credenciales tomadas del formulario
				$authAdapter->setIdentity( $form->getValue('username') );
				$authAdapter->setCredential( md5( $form->getValue('password') ) );
				
				// hace la autenticacion
				$auth = Zend_Auth::getInstance();
				$result = $auth->authenticate($authAdapter);
				
				if ($result->isValid())
				{
					// si es valido guarda los datos
					$data = $authAdapter->getResultRowObject();
					$auth->getStorage()->write($data);
					$this->_redirect('/registro');
				} else {
					// si falla no guarda nada y manda el mensaje de error
					$this->view->message = 'Nombre de usuario o contraseña incorrectos.';
				}
			}
		}
	}// fin loginAction()
	
	function logoutAction()
	{
		Zend_Auth::getInstance()->clearIdentity();
		$this->_redirect('/index');
	}
}