Framework Seam: autorización en aplicaciones

Como todos ya sabemos uno de los puntos importantes de una aplicación web es el control de la seguridad en cuanto a la autenticación y la autorización de usuarios.

Básicamente a la Autenticación le corresponde la tarea de ver quién es el usuario y a la Autorización le corresponde permitir o no el acceso a las distintas zonas de una aplicación web a partir, por ejemplo, de su perfil en la aplicación.

En este post me centraré en la Autorización, ya que Jboss Seam tiene mucho que decir al respecto, y os muestro una posible implementación para controlar el acceso de un usuario a una «zona» determinada de una aplicación.

Supongamos una aplicación con dos zonas, una zona de administración y una zona de acceso a usuarios. Para la primera zona (http://<aplicacion>/admin) se necesita que el usuario posea el rol «admin», mientras que para la segunda(http://<aplicacion>/usuario) el rol «usuario», nos centraremos en la primera, la segunda os la dejo como ejercicio…
También supongamos que tenemos un objeto «authenticator» que implementa los distintos métodos que requiere Seam respectos a la autenticación.
Normalmente, cuando un usuario entra en la aplicación sin haberse logado Seam lanza la excepción «NotLoggedInException» y posteriormente se redirige al usuario a la correspondiente pantalla para logarse. Este comportamiento se controla mediante el archivo «pages.xml»:
<exception class=»org.jboss.seam.security.NotLoggedInException«>
<redirect view-id=»/index.xhtml«>
<message>Usted debe estar logado para acceder a esta página.</message>
</redirect>
</exception>

Tras introducir los datos para el login (usuario y contraseña, certificado digital, etc.), el método «returnTo» del «autenticator» nos redirige a la zona adecuada, según observemos el rol que tiene asignado el usuario, esto lo podemos indicar en el fichero «components.xml»:
<event type=»org.jboss.seam.postAuthenticate«>
<action execute=»#{authenticator.returnTo}«/>
</event>

Una primera propuesta (adelanto que no es la definitiva), para controlar el acceso a la zona «admin» podríamos definir en el pages.xml lo siguiente:

<page  view-id=»/admin/*» login-required=»true»>
<restrict>#{s:hasRole(‘admin’)}</restrict>
</page>

<exception class=»org.jboss.seam.security.AuthorizationException«>
<end-conversation/>
<redirect view-id=»/index.xhtml«>
<message>Usted no posee los privilegios suficientes para acceder a esta zona.</message>
</redirect>
</exception>

Que en Castellano significa: para las peticiones a «/admin/*» comprueba que el usuario tiene el rol «admin». Cuando no lo tenga se lanzará la correpondiente excepción «AuthorizationException» y se le redirigirá a «/index.xhtml» mostrándose el mensaje «Usted no posee los privilegios suficientes para acceder a esta zona.»

Quizás si no vamos más allá podamos pensar que tenemos todo controlado, pero ahora imaginemos la siguiente situación:

1) Un usuario entra en la aplicación, se va a la página de Login (por el NotLoggedInException capturado) y se loga correctamente, sin rol «admin» por lo que no debería tener acceso a /admin, de hecho el «return True» le redirige a la zona pública.
2) El usuario que se cree un piratilla escribe a fuego en la barra de direcciones del navegador «/admin» y le da al botón de Enter…

¿Qué pasará?
Lo esperado es que si el usuario pide «manualmente» en /admin tras logarse y obtener únicamente el rol «usuario», la aplicación genera un AuthorizationException y se le redirigirá a /index.xhtml (pantalla inicial de bienvenida)…
Hazlo y comprobarás que no… el usuario tiene acceso a la zona de administración.

¿Y porqué? Pues una de las causas que he leído en el foro de jboss es que «esa excepción no se trata en la fase de render del JSF» y entonces no tiene el comportamiento que se indica en el pages.xml. Quizás no haya que buscar más porqués y proponer otra solución.
Una segunda propuesta, bastante atractiva aunque después propondré otra ligeramente más refinada, trata de que la aplicación capture correctamente la excepción AuthorizationException, nos redirija al index.xhtml y no suponga tener que redefinir arquitectura de ningún tipo… allá va:
Deberemos crear un nuevo «Controller» como este, especialmente dedicado a controlar el acceso a la zona de administración, quizás se podría usar cualquier controller ya existente, pero así queda más limpito nuestro código (vease «asignación de responsabilidades»):
@Name(«authorizationAdminController»)
@Scope(CONVERSATION)
@Restrict(«#{s:hasRole(‘admin’)}»)
public class AuthorizationAdminController extends ControllerJsfApplication {
public void forceCheckAdminAuthorization(){}
}

Vemos que queda un controller práctiamente vacío.
Debido a @Restrict sólo podrán «invocar» a este controller los Identities(usuarios logados) con rol «admin».

Ahora modificamos nuestro pages.xml de la siguiente forma:
Donde antes teníamos:
<page  view-id=»/admin/*» login-required=»true»>
<restrict>#{s:hasRole(‘admin’)}</restrict>
</page>

Ahora tenemos:
<page  view-id=»/admin/*» login-required=»true» action=»#authorizationAdminController.forceCheckAdminAuthorization}«>
</page>
Con lo que al hacer una petición a «/admin» se ejecutará el action ( authorizationAdminController.forceCheckAdminAuthorization ), el cual no podrá estar accesible si no se posee el rol «admin», debido a la anotación @Restrict del controller. Si el identity no posee este rol se lanza AuthorizationException, igual que pasaba antes sin usar el controller, sólo que ahora esta excepción si se captura correctamente y se realiza la redirección indicada en el pages.xml.
También deberemos mantener:
<exception class=»org.jboss.seam.security.AuthorizationException»>
<end-conversation/>
<redirect view-id=»/index.xhtml»>
<message>Usted no posee los privilegios suficientes para acceder a esta zona.</message>
</redirect>
</exception>
Con esta modificación conseguimos proteger correctamente la zona «/admin», un usuario que intente logarse y posteriormente entrar en «/admin», esta vez se le mostrará el index.xhtml con el mensaje: «Usted no posee los privilegios suficientes para acceder a esta zona.»
Una tercera solución es simplemente un refinamiento de la segunda, y llegamos a ella a través de la pregunta:
Si tenemos 50 zonas de protegidas, ¿tendremos que implementar 50 Controller semejantes a «AuthorizationAdminController» pero con un @Restrict distinto?
Pues no, la solución está en no anotar el Controller con @Restrict, sino anotar el propio método, así podríamos tener un único controller y N métodos para comprobar distintas autorizaciones:

@Name(«authorizationAdminController»)
@Scope(CONVERSATION)
public class AuthorizationAdminController extends ControllerJsfApplication {

@Restrict(«#{s:hasRole(‘admin’)}»)
public void forceCheckAdminAuthorization(){}

@Restrict(«#{s:hasRole(‘usuario’)}»)
public void forceCheckUsuarioAuthorization(){}

………

@Restrict(«#{s:hasRole(‘XXXX’)}»)
public void forceCheckXXXXXXXAuthorization(){}

}

Indicando en el pages.xml el método «forceCheck X Authorization» que corresponde a cada zona las protegeremos convenientemente.

Pues con esto ha acabado lo que ha intentado ser una explicación.

Nunca olvidaré esta «primera vez» en que escribí en xnoccio, espero que hallais disfrutado tanto como yo.

Que la suerte os acompañe !!