#192 Authorization with CanCan
- Download:
- source codeProject Files in Zip (107 KB)
- mp4Full Size H.264 Video (26.1 MB)
- m4vSmaller H.264 Video (17.3 MB)
- webmFull Size VP8 Video (43.9 MB)
- ogvFull Size Theora Video (36.9 MB)
Hace algunos episodios estudiábamos Declarative Authorization que si bien es un excelente plugin de Rails puede ser un poco pesado para sitios web más simples. Después de escribir el Railscast sobre Declarative Authorization Ryan Bates buscó una solución alternativa y, como no encontró ninguna, decidió implementar la suya propia, CanCan, tratando de mantenerlo todo lo más sencillo posible. Echémosle un vistazo.
En este episodio trabajaremos con la misma aplicación sencilla de blog que utilizamos en el episodio sobre Declarative Authorization. Esta aplicación tiene un cierto número de artículos, cada uno de los cuales pertenece a un usuario y que puede tener muchos comentarios asociados.
Obsérvese en la captura anterior que a pesar de que no hay una sesión iniciada los enlaces de edición y borrado de artículos están visibles. Queremos restringir, mediante la autorización, el acceso a lo que cada usuario puede hacer. Ya tenemos autenticación en nuestra aplicación de forma que los usuarios pueden darse de alta e iniciar sesión, para lo que hemos usado Authlogic aunque en realidad cualquier solución de autenticación nos serviría.
En la página de registro tenemos un número de checkboxes que permiten a un usuario escoger los roles a los que quiere pertenecer. Un administrador podrá hacer cualquier cosa; un moderador podrá editar los comentarios de cualquiera y un autor puede crear artículos y modificar los que haya escrito. Los usuarios que no pertenezcan a ningún rol pueden crear comentarios y cambiarlos.
Instalación de CanCan
CanCan es una gema. Para añadirlo a nuestra aplicación tenemos que añadir la siguiente línea en el bloque Rails::Initializer.run
de nuestro archivo /config/environment.rb
.
config.gem "cancan"
Nos podemos asegurar de que la gema está instalada ejecutando
sudo rake gems:install
Si ya tenemos instalada una versión antigua de CanCan tendremos que actualizarnos a la última porque hay algunas funcionalidades que sólo están presentes a partir de la versión 1.0.0.
Uso de CanCan
Para utilizar CanCan tenemos que crear una nueva clase llamada Ability
, que pondremos en nuestro directorio /app/models
. Esta clase tiene que incluir el módulo CanCan::Ability
y definir también un método initialize
que recibe un objeto de usuario como parámetro. Será en este método donde definiremos las capacidades de cada usuario.
class Ability include CanCan::Ability def initialize(user) end end
Las capacidades de cada usuario se definen con el método can
, el corazón de CanCan. Este método recibe dos parámetros, primero la acción que queremos realizar y segundo la clase de modelo sobre la que se aplica esta acción. Alternativamente podemos pasar :all
para aplicar una acción a cualquier modelo. Si queremos que todos los usuarios puedan leer todos los modelos podemos hacerlo de esta manera:
class Ability include CanCan::Ability def initialize(user) can :read, :all end end
Tal y como está nuestra autorización nadie puede editar o borrar artículos o comentarios, pero hay enlaces a las acciones edit
y destroy
en la página de cada artículo. En la vista para la acción show
del artículo podemos usar can?
(nótese el signo de interrogación) para determinar si el usuario actual está autorizado para realizar la acción a la que lleva cada enlace. Mientras que el método can
define las capacidades, can?
es un método booleano que determina si el usuario actual posee las capacidades. Al igual que can
, can?
recibe dos parámetros, una acción y un modelo, en este caso un Article
. Utilizaremos can?
en la vista show
de forma que los enlaces de edición y borrado estén ocultos a no ser que el usuario actual tenga la capacidad apropiada. Para hacerlo, envolveremos los enlaces con sentencias if
de forma que sólo se muestren cuando el usuario actual puede llevar a cabo la acción correspondiente sobre el artículo.
<p> <% if can? :update, @article %> <%= link_to "Edit", edit_article_path(@article) %> | <% end %> <% if can? :destroy, @article %> <%= link_to "Destroy", @article, :method => :delete, :confirm => "Are you sure?" %> | <% end %> <%= link_to "Back to Articles", articles_path %> </p>
Más abajo en la misma vista haremos un cambio similar en cada enlace de comentario.
<p> <% if can? :update, comment %> <%= link_to "Edit", edit_comment_path(comment) %> | <% end %> <% if can? :destroy, comment %> <%= link_to "Destroy", comment, :method => :delete, :confirm => "Are you sure?" %> <% end %> </p>
Con esto ya podemos cargar la página de un artículo y veremos que los enlaces para editar o borrar artículos y comentarios han desaparecido dado que no hay usuarios con la capacidad de editarlos.
Cómo proteger los Controladores
Las acciones siguen estando disponibles a pesar de que hayamos ocultado los enlaces para editar artículos y comentarios, por lo que si visitamos directamente la acción edit
de un artículo seguimos pudiendo cambiarlo. Así que además de hacer cambios en las vistas tendremos que modificar nuestros controladores para que los usuarios sólo pueden ejecutar las acciones para las que tienen autorización. Hay dos formas de hacerlo en CanCan. La primera funciona a nivel de acción y veremos un ejemplo con la acción edit
de ArticleController
.
def edit @article = Article.find(params[:id]) unauthorized! if cannot? :edit, @article end
Para evitar que se ejecute la acción invocamos a unathorized!
, que alzará una excepción. Por supuesto sólo querremos que esto ocurra cuando el usuario no esté autorizado y para verificar dicha autorización podemos utilizar el método can?
igual que hicimos en la vista o, como en este caso, cannot?
.
Si tratamos de acceder a la acción edit
directamente ahora recibiremos un error que nos impedirá hacerlo.
Podríamos repetir todo esto en cada acción de nuestros controladores, pero hay una forma más fácil de hacerlo porque estamos usando la convención REST. Al principio del controlador pordemos llamar a load_and_authorize_resource
que cargará y autorizará al recurso apropiado en un filtro. Dado que este método carga el recurso basándose en la acción podemos eliminar las líneas de código que hay en cada acción para poner dicho recurso en una variable de instancia (en este caso era @article
) haciendo que el código de ArticlesController
tenga este aspecto:
class ArticlesController < ApplicationController load_and_authorize_resource def index @articles = Article.all end def show @comment = Comment.new(:article => @article) end def new end def create @article.user = current_user if @article.save flash[:notice] = "Successfully created article." redirect_to @article else render :action => 'new' end end def edit end def update if @article.update_attributes(params[:article]) flash[:notice] = "Successfully updated article." redirect_to @article else render :action => 'edit' end end def destroy @article.destroy flash[:notice] = "Successfully destroyed article." redirect_to articles_url end end
Con esto los usuarios no podrán crear, editar o borrar ningún artículo.
Queremos hacer un cambio parecido en CommentsController
para restringir el acceso a sus acciones, y una vez más utilizaremos load_and_authorize_resources
para cargar el recurso Comment
y comprobar su autorización. Si Comment
fuese en nuestras rutas un recurso anidado de Article
podríamos utilizar :nested
en load_and_authorize_resources
para cargar los comentarios a través del recurso Article
.
load_and_authorize_resource :nested => :article
En nuestro caso no tendremos que hacerlo porque no estamos anidando los recursos.
Añadiendo Capacidades
Ahora que nuestra aplicación es segura podemos empezar a definir las capacidades que tendrá cada rol. Esto se hace en la clase Ability
que añadimos anteriormente, definiéndolas en el método initialize
para que sean reflejadas en el resto de la aplicación.
<p><code>initialize</code> recibe el usuario actual, de forma que podamos modificar las capacidades dependiendo de qué usuario está activo. Vamos a empezar con los usuarios que tienen el rol de administrador que son los que podrán hacer cualquier cosa. Este usuario puede ser un objeto de cualquier tipo lo que significa que la autenticación está completamente desacoplada de la autorización. Por ejemplo, lo que define a un usuario como administrador depende por completo del sistema de autenticación. Podríamos tener un campo booleano llamado <code>admin?</code> en nuestro modelo <code>User</code>.</p> <p>En nuestra aplicación un usuario puede tener varios roles y tenemos un método <code>role?</code> que nos dice si un usuario pertenece a un rol. Utilizaremos este método para establecer las capacidades.</p> ``` ruby class Ability include CanCan::Ability def initialize(user) if user.role? :admin can :manage, :all else can :read, :all end end end
Nuestro código ahora comprueba si el usuario actual es administrador y si lo es le permite administrar todos los modelos. Si se pasa :manage
como acción quiere decir que se pueden efectuar todas las acciones sobre un modelo.
Aún tenemos que definir el método role?
en el modelo User
. Hemos establecido los roels de la misma manera que hicimos en el Episodio 189 [ver, leer] pero no importa cómo definamos nuestros roles siempre que podamos determinar a qué rol pertenece un usuario.
class User < ActiveRecord::Base acts_as_authentic has_many :articles has_many :comments named_scope :with_role, lambda { |role| {:conditions => "roles_mask & #{2**ROLES.index(role.to_s)} > 0 "} } ROLES = %w[admin moderator author editor] def roles=(roles) self.roles_mask = (roles & ROLES).map { |r| 2**ROLES.index(r) }.sum end def roles ROLES.reject { |r| ((roles_mask || 0) & 2**ROLES.index(r)).zero? } end def role?(role) roles.include? role.to_s end end
El método role?
que hemos añadido comprueba que los roles del usuario incluyan el rol pasado
Volvamos a la clase Ability
, donde aún nos queda por hacer un cambio más. En initialize
comprobamos si el usuario pertenece al rol pero los usuarios invitados (que aún no se han registrado) user
será nil
. Podríamos comprobar la presencia de nil
antes de chequear los roles del usuario pero en lugar de esto crearemos un usuario invitado si el usuario pasado es nulo. De esta forma podremos invocar a métodos como role?
para usuarios que aún no se hayan dado de alta.
class Ability include CanCan::Ability def initialize(user) user ||= User.new # Guest user if user.role? :admin can :manage, :all else can :read, :all end end end
Si volvemos otra vez a la aplicación aún seguiremos sin poder editar o destruir comentarios pero si iniciamos sesión como un usuario que tenga el rol de administración, se mostrarán los enlaces.
Como ya tenemos funcionando la autorización para administradores ahora tendremos que establecer las capacidades para el resto de roles. Empezaremos con los usuarios invitados, que no tienen roles asignados. Estos usuarios deberían poder crear comentarios y actualizar los comentarios que hayan escrito. Para hacer esto tendremos que modificar la clase Ability
de esta manera:
class Ability include CanCan::Ability def initialize(user) user ||= User.new if user.role? :admin can :manage, :all else can :read, :all can :create, Comment can :update, Comment do |comment| comment.try(:user) == user end end end end
El código para permitir que los usuarios invitados creen comentarios es muy sencillo, pero el código de actualización es un poco más complicado porque los usuarios deberían sólo poder actualizar los comentarios que hayan escrito. Para hacerlo le pasaremos a can
un bloque que a su vez pasará la instancia del modelo que estamos comprobando. Este bloque debería devolver true
o false
dependiendo de si la acción debe permitirse o no, por tanto es aquí donde comprobaremos si el usuario del comentario es el usuario actual. Es posible que el comentario sea nulo, así que utilizaremos el método try
de Rails para leer el atributo user
de forma que se devuelva nil
en lugar de elevar una excepción.
Si iniciamos una sesión como un usuario que no tiene roles, veremos que podemos añadir comentario y actualizarlo, pero no en los comentarios de los demás.
A continuación modificaremos el código para añadir las capacidades de los moderadores, que deberían poder modificar cualquier comentarios. Actualizaremos la capacidad de actualización de comentarios para permitirlo.
can :update, Comment do |comment| comment.try(:user) == user || user.role?(:moderator) end
Aún nos queda un rol más por cubrir, :author
. Los autores deberían pdoer crear artículos y modificar cualquier artículo que hayan escrito. Tan sólo tenemos que añadir el siguiente código en la clase Ability
:
class Ability include CanCan::Ability def initialize(user) user ||= User.new if user.role? :admin can :manage, :all else can :read, :all can :create, Comment can :update, Comment do |comment| comment.try(:user) == user || user.role?(:moderator) end if user.role?(:author) can :create, Article can :update, Article do |article| article.try(:user) == user end end end end end
Tal y como hicimos con los usuarios invitados y los comentarios, en la capacidad de actualización de artículo le pasamos el artículo actual a un bloque y comprobamos que el autor del artículo sea el usuario actual.
Ya tenemos definidas todas las capacidades para todos los roles de usuario. Lo mejor de CanCan es que nos permite definir todas las capacidades en una ubicación única, y el resto de la aplicación reflejará estos cambios.
Para embellecer la página de error
Cuando un usuario invoca una acción para la que no tiene acceso verá un página de error bastante fea mostrando una excepción AccessDenied
. Podemos cambiarlo para que se vea una página de error con un aspecto mejor:
Rails proporciona un método llamado rescue_from
que podemos poner en nuestro ApplicationController
, que recibe una excepción y un bloqueo o método. Nosotros le pasaremos un bloque dentro del cual haremos que la aplicación muestre un mensaje flash y redirija a la página de inicio.
rescue_from CanCan::AccessDenied do |exception| flash[:error] = "Access denied!" redirect_to root_url end
Si un usuario sin roles intenta editar un artículo tecleando directamente la URL será redirigido a la página de inicio con un mensaje diciéndole que no puede hacerlo.
Eso es todo por este episodio. Para más detalles o para informar de un problema, visiten la página de GitHub que ha creado Ryan para el proyecto.