homeASCIIcasts

192: Autorización con CanCan 

(view original Railscast)

Other translations: En It Pt

Written by Juan Lupión

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.

La página de un artículo en nuestra aplicación

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.

La página de registro con los checkboxes de roles.

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íuculo 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.

Ya no se ven los enlaces.

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.

Se producirá una excepción si se intenta acceder a una acción no autorizada.

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.

  class Ability
     include CanCan::Ability

     def initialize(user)
       can :read, :all
     end
   end

initialize 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 admin? en nuestro modelo User.

En nuestra aplicación un usuario puede tener varios roles y tenemos un método role? que nos dice si un usuario pertenece a un rol. Utilizaremos este método para establecer las capacidades.

  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.

Los administradores vuelven a ver 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.

Los usuarios invitados sólo pueden editar sus propios comentarios.

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.

Se muestra un mensaje flash si se intenta acceder una acción no permitida.

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.