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 !!

Comentarios

  1. Hola,
    estoy haciendo una aplicación web y requiere de controles de roles.
    Tengo una classe «Club» en donde estan los clubs de una competición. Estos clubs tienen un usuario y contraseña.
    A través del método Authenticator, controlo si el usuario-password es correcto y se le permite la entrada a la pagina o no. Este seria un tipo de rol, «el rol de club».
    Si ahora quiero tener un «rol de administrador», ¿qué pasos debo seguir?.
    Tu explicación la entiendo bastante,pero cómo identificar que un usuario es de tipo club y otro de tipo administrador?,¿dónde tengo que hacer la discriminación de los roles?

    Muchas gracias por tu ayuda

  2. Hola Xavier,
    No se si he entendido bien tu pregunta, pero las forma mas sencilla de discriminar es restringiendo las urls a las que tiene acceso el usuario (page) como se indica en el post, o simplemente ocultando la opción al usuario si no tiene el rol necesario, ej:
    <s:link action=»#{manager.comprar}» rendered=»#{s:hasRole(’club’) and s:hasRole(‘admin’)}» …/>

  3. Gracias Félix por tu respuesta pero me siguen algunas dudas….
    1. En la web podran entrar cualquier «Navegador» del mundo, pero este «Navegador» sólo podrà ver algunas páginas,¿cóm hacerlo?. Tienes que saber que un «Navegador» no está logeado ni nada por el estilo, ni tenemos ninguna información suya.Será cualquier persona con internet que entré en mi web.
    2. El «Navegador» puede pasar a ser un «Usuario registrado» una vez se dé de alta. Así pues tendremos un «Usuario registrado» (que será un Club). Ahora el «Club usuario registrado» podrà entrar en otras páginas si se logea correctamente, pero ,¿cómo hacerlo?. Tener en cuenta que ahora si que tenemos datos en una clase y en la bbdd del «club usuario registrado».
    3. Tenemos una clase «Administrador» en el sistema que contiene su nombre de usuario y su password. Al logearse el «Administrador» tendrá derechos de ver ciertas páginas, pero, ¿cóm hacerlo?.

    Muchas gracias por tu respuesta y perdoname se reitero mis comentarios.

  4. Hola Xavier, intentaré ayudarte, te voy explicando por pasos:
    1. Puedes tener una zona de la aplicación web en la que no restrinjas el acceso, por ejemplo «/publico». Y tener en «/club» la zona en la que el usuario necesite el rol «club», por ejemplo.
    2. Si el usuario se registra, obtendrá el rol «Club» al logarse y por tanto tendrá visible también la zona pública y la zona privada.
    3. Para los administradores puedes tener, de igual forma, una tercera zona «/admin».

    Te recuerdo, ya te lo comentó Félix, que también tienes diponible el atributo rendered de los tags, lo puedes utilizar para visualizar/ocultar botones o cualquier otro componente, puedes usar este atributo así, por ejemplo:
    t:commandButton rendered=»#{s:hasRole(’club’)}»

    Puedes usar esto para ocultar botones o acciones (por ejemplo) según si el usuario actual en la aplicación tiene rol «club» o «admin».

    Si sigues teniendo dudas, es que existes, como decía Aquel… cuéntanos, no es ninguna molestia, al contrario.

    Un Saludo.

  5. Ante todo gracias por vuestra ayuda y paciencia.
    Debo deciros que la web que estoy haciendo es el proyecto de carrera (¡¡lo presento en junio!!). Y mi professor me aconsejó hacerlo con JBoss Seam y Hibernate. De ahí que tenga tantas dudas ya que no lo domino mucho (no se tocan en la carrera).
    Ahí van mis otras dudas:
    1. En la clase «Authenticator» tengo una sola función, la «boolean authenticate». La función me devuelve true/false si al logearse un usuario (sea club o administrador) es correcto o no.
    En esta función utilizo el «identity.addRole(«admin»)» y «identity.addRole(«user»)» dependiendo de quien entre (admin=Administrador,user=Club).

    Supongo que hasta ahí ningún problema.

    Ahora, por ejemplo, tengo en mi web 4 páginas (inicio,pág A,pág B y pág C).
    Si quiero que el rol «admin» pueda entrar en la página inicio y en la A y B,¿qué debo hacer,qué pasos seguir?.
    Y si ahora quiero que el rol «user» pueda entrar en la página inicio y en la B y C,¿qué debo hacer,qué pasos seguir?.
    Y si ahora quiero que cualquier persona, sin logerase ni nada,peda entrar en la página inicio¿qué debo hacer,qué pasos seguir?.

    Muchas gracias por dedicar parte de vuestro timepo en ayudareme.
    Por cierto si quereis escribirme en el mail,teneis mi permiso.Lo digo por si vuestras explicaciones són muy largas y no las quereis escribir por aquí.
    Muchas gracias otra vez.

  6. A ver, te cuento una posibilidad, oganiza el sitio web así:

    /Inicio.jsf
    /A/a.jsf
    /B/b.jsf
    /C/c.jsf

    Depués usa la configuración sobre autorización convenientemente, para asegurar que:
    /A/* necesita uno o varios roles(puedes concatener usando «and»)
    Lo mismo para /B/* y para /C/*.

    /inicio.jsf como lo va a «ver» todos los usuarios lo puedes ignorar y no requerir ningún rol.

    Saludos.

  7. Hola,
    mis páginas són del tipo «.xml», así que mis páginas serian «Inicio.xhml»,»A.xhml», «B.xhml», «C.xhml». Hasta aquí supongo que todo correcto.
    Pero qué configuración de autorización utilizar?, yo uso la función «authenticate» de la clase «Authenticator.java». En esta clase sé diferenciar entre un «club» y el «administrador».
    El problema que tengo, porque no lo sé hacer, es:
    ¿cómo le digo a la página A qué roles la pueden «ver»?,¿a qué fichero («components.mxl»??,»pages.xml»??,otras??) tengo que ir y qué tengo que añadir en el mismo?

    Muchas gracias y saludos

  8. Bueno, en la fase de autenticación le tienes que asignar el rol correspondiente al usuario.

    La forma de «proteger» las distintas páginas para una estructura como esta:
    Inicio.xhtml
    A/A.xhtml
    B/B.xhtml
    C/C.xhtml

    una posible configuración cobre autorización en el acceso puede ser:
    1) En pages.xml

    <page view-id=”/A/*” login-required=”true” action=”#authorizationAdminController.forceCheck_A_Zone_Authorization}“>
    </page>

    <page view-id=”/B/*” login-required=”true” action=”#authorizationAdminController.forceCheck_B_Zone_Authorization}“>
    </page>

    <page view-id=”/C/*” login-required=”true” action=”#authorizationAdminController.forceCheck_C_Zone_Authorization}“>
    </page>

    2) En la clase de autorización tener estos tres métodos restringidos a los roles necesarios, por ejemplo:

    @Restrict(”#{s:hasRole(’ROL_X’)} and #{s:hasRole(‘ROL_Y’)} ”)
    public void forceCheck_X_Zone_Authorization(){}

    Igual que «and» se pude usar «or» y combinarlas de manera adecuada.

    Un Saludo.

  9. Buenas Manuel,
    …pues va a ser que no…yo ya no sé si tirar el ordenador o qué!!???

    Te cuento el ejemplo (entrar en la página CompeticioEdit) que he probado.

    A ver:
    voy a «pages.xml» y pongo:

  10. sigue del comentario 10 (no sé que pasó).

    voy a «pages.xml» y pongo:

    Y luego voy a la clase Authenticate y pongo:
    @Restrict(”#{s:hasRole(’admin’)}”)
    public void forceCheck_A_Zone_Authorization(){}

    Pues resulta que en la página CompeticioEdit en cuestión pueden entrar todo tipo de usuarios, cuando yo quiero que solo entren usuarios con el rol=’admin’.

    ¿Qué puedo hacer?. Es que estoy de los nervios porque en 15 dias tengo que entregar el proyecto de la carrera.

    Muchas gracias por tu ayuda.

  11. (No sé que pasa hoy en esta página que no se actualizan como deberian)

    Te pongo lo que he puesto en el archivo «pages.xml».

  12. en el archivo «pages.xml» pongo:

    page view-id=”/CompeticioEdit/*” login-required=”true”
    action=”{authorizationAdminController.forceCheck_A_Zone_Authorization}“

  13. Hola,

    a parte de que falta una almohadilla delante de la primera llave del action, todo es correcto.
    Comprueba que el método forceCheck… esté anotado convenientemente con @Restrict y el rol correspondiente.

    Asegúrate de asignar el rol «admin» sólo a los usuarios debidos y no a todos, puede ser una de las causas por la que todos los usuarios tengan acceso.
    Debugea y mira si los roles se asignan correctamente.

    Suerte…

  14. Muchas gracias,ya encontré el problema.
    Tengo que poner:
    page view-id=”/CompeticioEdit.xhtml/*”
    y no:
    page view-id=”/CompeticioEdit/*”

    Ahora ya sólo me queda un par de problemillas.
    Mi web es para hacer inscripciones de clubs.Pero esta inscripción se hace en tres estados por separado:
    1)mostrar interés: entramos unos datos
    2)preinscripción: entramos otros datos
    3)inscripción: entramos otros datos.

    Lo que quiero es:
    a)cuando termine de entrar los datos del interés, al apretar el botón de hacer alta, quiero que un campo llamado «interes» se actualice directamente al valor «si».
    b)cuando termine de entrar los datos de la preinscripcion, al apretar el botón de hacer alta, quiero que un campo llamado «pre» se actualice directamente al valor «si».
    c)cuando termine de entrar los datos de la inscripcion, al apretar el botón de hacer alta, quiero que un campo llamado «inscripcion» se actualice directamente al valor «si».
    d)Si borro un club en cualquier estado, quiero que al apretar el botón de borrar,me actualice al valor «no»,los tres campos mencionados anteriormente.

    Y otro tema que me gustaria saber es como la web puede saber y guardar el dia y hora del momento.
    Esto me servirá posteriormente para cuando una club muestre «interés» por la competición,tendrá por ejemplo,10 dias para darse de alta en preinscripcion. Y poder controlar de esta forma las inscripciones.

    ¿Cómo debo hacerlo?
    Muchas gracias por toda tu ayuda,crack!!

  15. Una última duda:

    En la web, a la hora de hacer la alta de un club, te expliqué que se hacía en tres etapas:interes, preinscripción y inscripción.

    Bien,en otro orden de cosas, también tendremos jugadores que pertenecen a este club.
    Me gustaria que sólo pudiera hacer la alta de un jugador una vez haya hecho la alta de un club en la etapa de preinscripción.Es decir, sólo cuando un club haya hecho la alta en la etapa de preinscripción podré hacer la alta de sus jugadores.¿Cómo hacerlo,cómo controlarlo?.

    Asismismo, hay una entidad «Bote». Sólo podré hacer la alta de un bote cuando el club haya sido dado de alta en el modo inscripción. Eso lo controlaré de la misma forma que antes.Pero…al dar de alta de un bote, este pertenece a un club y al poner a los jugadores que van en el Bote, sólo querria ver a los jugadores que pertenecen al club en concreto..¿Cómo hacerlo,cómo controlarlo?.

    Muchas gracias por tu ayuda.

  16. El comentario 15 ya lo tengo hecho.
    El comentario 16 todavia lo tengo pendiente.

    Ahora, otro problemilla que tengo es el siguiente:
    -Cuando entro un Club (en la etapa interes) y le pido el «password», quiero hacer que abajo lo vuelvan a entrar para hacer la confirmación del mismo.¿Cómo debo hacerlo?.

    Muchas gracias por tu ayuda.

  17. Cuando digo confirmar es que escriba el mismo string en el password y en la confirmación de password.
    Ej:
    password=HOLA
    confirmación password=HOLA

    Lo que quiero es ver si el string de ‘confirmación password’==’password’

    Gracias

  18. Tengo un caso que se dá mucho y que no veo como resolverlo con este esquema propuesto.

    Por ejemplo yo tengo usuarios y roles relacionados. Entonces a nivel de la página deseo que un determinado campo sea visible siempre y cuando el usuario posea un rol que tenga acceso a ese campo. Pero no mediante hard code sino que esa «definicion» (de que tal campo para tal rol es visible) sea definida mediante tablas en una base de datos y luego mantenidas por una aplicacion.
    En la página se pondria el campo y el sistema automáticamente deberia ser capaz de saber si ese campo debe estar visible o no para el usuario actual segun los roles asignados a el.
    Me explico ?

  19. Bueno, eso se puede resolver de una forma distinta.

    Los distintos tags jsf (por ejemplo los de la librería Tomahawk) tienen un atributo que es «rendered», éste indica si el componente jsf se pinta o no se pinta (true o false respectivamente).
    Puedes hacer lo siguiente:
    t:inputText … rendered=»#{beanController.isRendered}»

    En el beanController es un típico bean de sesión en el que debes implementar la función «boolean getIsRendered()», en esa función lo único que debes hacer es mirar si el usuario logado (en sesión) tiene los permisos(roles) necesarios para pintar ese componente€, accediendo a BD para consultarlo (según me has explicado antes).

    Puedes tener varias funciones similares getIsRendered para distintos componentes jsf, si es que cada uno de ellos evalua distintas condiciones para pintar dicho elemento, sino puedes usar la misma función en todas.

    Espero haberte ayudado. En cualquier caso haz uso del atributo «rendered».

    Saludos.

  20. En vez de usar una funcion «isRendered» puedes poner:
    rendered=»#{s:hasRole(‘RolX’)}» y combinar con operaciones lógicas directamente.

    Para gustos los colores…

    Saludos.

  21. Muy buena tu respuesta, pero la verdad ya conocia esa solución.
    Pero en mi aplicación , si el cliente desea que un usuario perteneciente al rol A pueda ver el campo NOMBRE del formulario PEPE pero que si el usuario pertenece al rol B ese campo solo lo vea pero no pueda modificarlo. Y esta información sea el cliente que la configura mediante un formulario al mantener los datos del rol. No se si me explique bien.

  22. Agrego comentario al anterior. Los roles y usuarios son creados por el usuario y no estan Hard Coded. Es decir una instruccion tipo. rendered=»isUserInRol(«A»)» no me sirve.

  23. Yo lo tengo implementado de una forma que en la página solamente ingreso.

    Internamente, al renderizar el componente el mismo se fija si se puede mostrar o no segun el rol al cual pertenesca el usuario y los permisos otorgados al rol.

    Los permisos serian:
    Rol -> Lista de Permisos

    donde:

    Permiso=
    IdRecurso=Pagina.Campo
    Enable=true/false

    Algo asi.
    Saludos

  24. creo q podria hacerse con disabled no estoy seguro pero seria asi:

    disabled=»#{s:hasRole(‘B’)}»

    dependiendo del rol q sea

  25. Hola pueblo, disculpen, yo estoy iniciando en el tema, y queria uan ayudita. Quero saver como puedo utilizar el Authenticator, para que un usuario no se pueda logear 2 o mas veces… o loguear un nuevo usuario, teniendo otro logueado y sin aver echo logout.
    Espero qeu se entienda mi pregunta.. muchas gracias.. y a la espera de una buena respuesta.. 🙂

  26. Hola Raúl,
    perfectamente puedes usarlo para lo que te interesa.

    Puedes poner en Sesión un parámetro en el momento en que haga Login el usuario para controlar posteriormente si intenta hacer un segundo Login. Para saber si hay un usuario logado en un instante determinado te basta con hacer «Identity.instance().isLoggedIn()».
    Todas estas comprobaciones las podrías meter dentro de un método en el Authorizator y en el caso de que no se cumplieran las condiciones que indicas podrías lanzar una excepción de tipo «org.jboss.seam.security.AuthorizationException», que es la misma que se lanza cuando se violan los @Restrict.

    Ahora no tendrías un método anotado con @Restrict, sino que el método quedaría algo como:
    public void checkLogin() {
    if(!condicionesParaLogin()){
    throw new AuthorizationException(«Debe cerrar el navegador…»);
    }
    }

  27. Hola a todos…

    Soy nuevo en este espacio ya tengo algunos meses metiendole mano al SEAM, pero hay algo que aun no puedo resolver…

    @gcasadevall, seria de mucha ayuda que me des mas detalles de la solución que propones, creo que seria un punto de partida para dar solución a la situación de roles dinámicos, es decir:
    Yo en mi aplicación a través de un formulario creo un rol y le asigno los permisos que deseo que tenga este nuevo rol… El problema esta que al usar una etiqueta de este tipo s:hasRole(’ADMIN’) el rol me queda en Hard Code y no he encontrado la forma de como hacer que este comportamiento sea dinámico y pueda comportarse de acuerdo al rol que he creado en la aplicación.

    Disculpen si la pregunta es muy larga o muy fácil de resolver…

    Saludos y Gracias

  28. Hola a todos.

    No encuentro la clase ControllerJsfApplication. La he buscado por cielo y tierra y nada…
    ¿Dónde la puedo conseguir?

    Gracias.

Comments are closed.