#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)
Alguns episódios atrás nós falamos sobre o Declarive Authorization. Embora ele seja um excelente plugin de autorização para o Rails, ele pode ser um pouco pesado para sites mais simples. Depois de escrever o Railscast sobre Declarative Authorization, Ryan Bates buscou uma solução alternativa e, não conseguindo encontrar uma que se adaptasse às suas necessidades, decidiu escrever a sua solução, CanCan. CanCan é um plugin de autorização simples e Ryan tentou manter tudo o mais simples possível enquanto estava desenvolvendo. Vamos dar uma olhada.
Nesse episódio, vamos trabalhar com a mesma aplicação de um blog básico que usamos no episódio do Declarative Authorization. A aplicação tem uma série de artigos, cada qual pertence a um usuário e pode ter muitos comentários associados.
Repare na imagem acima que, embora não haja usuário logado, os links para editar e destruir artigos e comentários são visíveis. Queremos utilizar as autorizações para restringir o acesso ao que cada usuário pode fazer. Já temos a autenticação no aplicativo para que os usuários possam se inscrever e efetuar o login. Usamos Authlogic para fazer isso, mas qualquer solução de autenticação funciona.
Na página de login, temos uma série de caixas de seleção para permitir que o usuário selecione os papéis que ele quer ter. Um administrador terá permissão para fazer tudo, um moderador pode editar comentários de qualquer pessoa e um autor pode criar artigos e atualizar os artigos que escreveu. Os usuários que não pertencem a nenhum papel podem comentar e atualizar os seus comentários.
Instalando o CanCan
CanCan is fornecido como uma gem. Para adicioná-lo a sua aplicação, precisamos adicionar a seguinte linha ao bloco
Rails::Initializer.run
do arquivo /config/environment.rb
.
config.gem "cancan"
Podemos então ter certeza de que a gem está instalada executando:
sudo rake gems:install
Se você tinha instalado uma versão anterior do CanCan, você precisa ter certeza de que atualizou para a versão mais recente, pois há algumas funcionalidades que só estão disponíveis a partir da versão 1.0.0.
Usando o CanCan
Para usar o CanCan, precisamos criar uma nova classe chamada Ability
, que vamos colocar na pasta /app/models
. A classe tem que incluir o módulo CanCan::Ability
e também o método initialize
, que recebe um objeto usuário como parâmetro. É nesse método que vamos definir as habilidades para cada tipo de usuário.
class Ability include CanCan::Ability def initialize(user) end end
As habilidades são definidas com o método de três letras "can", o qual é o coração do CanCan. Esse método tem dois parâmetros: o primeiro é a ação que desejamos realizar, e o segundo é a classe de modelo em que a ação será aplicada. Alternativamente, para aplicar uma ação a todos os modelos, podemos passar :all
. Se queremos que todos os usuários sejam capazes de ler todos os modelos, podemos fazer isso:
class Ability include CanCan::Ability def initialize(user) can :read, :all end end
Na nossa definição de autorização, ninguém pode editar ou apagar artigos ou comentários, mas ainda há os links para as actions edit e destroy na página de cada artigo. No código da view da action show do artigo, podemos usar o método can?
(note o ponto de interrogação) para determinar se o usuário atual está autorizado a executar a action de cada link. Enquanto o método can
pode definir as habilidades, can?
é um método booleano que determina se o usuário atual tem essa habilidade. Como can
, can?
tem dois parâmetros, uma action e um modelo, nesse caso, um artigo. Usaremos can?
na view show para que os links de editar e apagar fiquem ocultos a menos que o usuário atual tenha a habilidade adequada. Para fazer isso, vamos colocar os links de artigos dentro de um comando if
de forma que eles só serão mostrados se o usuário atual puder executar a ação apropriada em um artigo.
<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>
Mais abaixo, no código da mesma view, vamos fazer uma mudança semelhante para cada link de comentário.
<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>
Podemos agora carregar a página de um artigo e ver que os links para editar ou apagar artigos e comentários sumiram, pois não há usuários com a habilidade de editar ou alterá-los.
Protegendo os Controllers
Embora tenhamos removido os links para editar artigos e comentários as próprias actions ainda estão disponíveis e se visitarmos diretamente a action edit para um artigo, ainda podemos atualizá-lo. Portanto, assim como fizemos alterações na camada view, será necessário modificarmos nossos controllers de modo que usuários possam acessar apenas as actions que estão autorizados. Existem duas maneiras de fazermos isso com o CanCan. A primeira funciona no nível de uma action e vamos usar a action edit do ArticleController
como exemplo.
def edit @article = Article.find(params[:id]) unauthorized! if cannot? :edit, @article end
Para parar a action que está sendo executada, chamamos unauthorized!
, que irá gerar uma exceção. Obviamente, nós só queremos essa exceção levantada se o usuário não tem a devida autorização. Para verificar a autorização, podemos usar can
como fizemos na view ou, como temos aqui, cannot?
.
Se tentarmos acessar a action edit diretamente agora, seremos interrompidos e uma exceção será lançada.
Poderíamos repetir isso em todas as actions de nossos controllers, mas como estamos usando a convenção RESTful, há uma maneira mais fácil de fazer isso. Na parte superior dos controllers, podemos chamar load_and_authorize_resource
que irá carregar e autorizar o recurso adequado em um before filter. Como esse método carrega o recurso necessário para nós, com base na action, podemos eliminar as linhas de código que criam uma variável de instância para cada action (nesse caso, @article), tornando nosso ArticlesController
parecido com isso:
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
Com isso os usuários existentes não serão capazes de criar, editar ou excluir todos os artigos.
Nós vamos ter que fazer uma mudança semelhante no CommentsController
para restringir o acesso às suas actions também. Mais uma vez vamos usar load_and_authorize_resources
para carregar um recurso Comment
e verificar a autorização para ele. Se o Comment
é um recurso aninhado com Article
nas rotas, podemos usar: :nested
com o load_and_authorize_resources
para carregar os comentários através do recurso Article
.
load_and_authorize_resource :nested => :article
Não estamos usando aninhamento aqui, dessa forma, não precisamos fazer isso.
Adicionando Habilidades
Agora que nossa aplicação é segura, podemos começar a definir as habilidades que cada papel terá. Isso é feito na classe Ability
que criamos anteriormente. As habilidades que definimos no método initialize
será refletida pelo resto da nossa aplicação.
class Ability include CanCan::Ability def initialize(user) can :read, :all end end
Passamos o usuário atual para o método initialize
assim nós podemos mudar as habilidades de acordo com o usuário logado no momento. Vamos começar com os usuários na função de administrador, que devem ser capazes de gerenciar tudo.
O usuário passado para o método initialize
pode ser objeto de qualquer tipo, o que significa que a autenticação é completamente dissociada da autorização. O que define um usuário como, por exemplo, um administrador depende inteiramente do sistema de autenticação utilizado. Poderíamos, por exemplo, ter um método boolean admin?
no nosso modelo User
. Em nossa aplicação, o usuário pode ter muitos papéis e nós vamos ter um método role?
para nos dizer se o usuário é membro de um papel. Vamos usar esse método para definir as habilidades.
class Ability include CanCan::Ability def initialize(user) if user.role? :admin can :manage, :all else can :read, :all end end end
Nosso código agora verifica se o usuário atual é um administrador e, se assim permite gerenciar todos os modelos. Passando :manage
como uma ação, significa que o usuário pode executar todas as ações em um modelo.
Ainda precisamos definir o método role?
no modelo User
. Nós criamos os papéis aqui da mesma forma que fizemos no Episódio 189 [assistir, ler], mas não importa como você configura os seus papéis, desde que você possa determinar quais os papéis ou funções que um usuário pertence.
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
O método role?
que nós adicionamos aqui, verifica que o papel passado está incluído nos papéis do usuário.
De volta à classe Ability
, precisamos fazer mais uma mudança. No método initialize
estamos verificando se o usuário pertence a um papel, mas para usuários visitantes - que não estão logados ainda por exemplo - user
será nil
. Poderíamos verificar se é nil
antes de tentarmos verificar o papel do usuário, mas em vez disso, vamos criar um usuário convidado, se o usuário passado for nil
. Desta forma, ainda podemos chamar métodos como o role?
para os usuários que ainda não tenham criado uma conta.
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
Se voltarmos para a nossa aplicação novamente, ainda não somos capazes de editar ou apagar comentários, mas se logarmos como um usuário com o papel de admin, então os links serão mostrados.
Tendo conseguido a autorização funcionando para os administradores, precisamos agora criar as habilidades para os outros papéis. Vamos começar com usuários convidados, aqueles que não têm funções atribuídas a eles. Eles devem ser capazes de criar comentários e atualizar comentários que escreveram. Para fazer isso vamos alterar a nossa classe Ability
da seguinte forma:
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
Escrever o código para habilitar usuários convidados a criar comentários é simples, mas o código de atualização é um pouco mais complicado, pois os usuários só devem ser capazes de atualizar os comentários que escreveram. Para fazer isso, podemos passar um bloco que vai passar um objeto do modelo que estamos verificando. O bloco deve retornar true
ou false
dependendo se o recurso deve ser permitido. No bloco vamos verificar se o dono do comentário é o usuário atual. Há a possibilidade do comentário não ter dono, então vamos usar o método try
do Rails para tentar ler o atributo usuário de forma que nil
é devolvido se o usuário é nulo em vez de uma exceção ser lançada.
Se logarmos como um usuário que não tem papéis agora, podemos adicionar um comentário e atualizá-lo, mas não os comentários feitos por outros.
Agora vamos modificar o código e adicionar as habilidades para moderadores. Moderadores devem poder modificar quaisquer comentários, então vamos modificar a habilidade de atualizar comentários para permitir isso.
can :update, Comment do |comment| comment.try(:user) == user || user.role?(:moderator) end
Nós temos um papel a mais para cobrir: o autor. Os autores devem poder criar artigos e modificar todos os artigos que eles escreveram. Para adicionar essas capacidades, só precisamos adicionar o seguinte código para a classe 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
Como fizemos com os usuários visitantes e comentários, passamos o artigo atual para um bloco e na habilidade de atualização de artigo verificamos se o dono do artigo é o usuário atual.
Agora temos todas as nossas habilidades definidas para cada papel de usuário. O bom do CanCan é que ele nos permite definir todas as habilidades em um só local e as alterações serão refletidas no resto da aplicação.
Uma Página de Erros Mais Bonita
Se um usuário solicita uma ação que não tem acesso, verá uma página de erro bastante feia mostrando uma exceção AccessDenied
. Nós podemos mudar isso para que ele veja uma página de erro melhor, com aparência personalizada.
O Rails fornece um método chamado rescue_from
que podemos colocar em nosso ApplicationController
. Vamos passar um bloco e dentro fazer a aplicação mostrar uma mensagem de erro e redirecionar para a página inicial.
rescue_from CanCan::AccessDenied do |exception| flash[:error] = "Access denied!" redirect_to root_url end
Se um usuário sem papéis agora tentar editar um artigo digitando a url diretamente, ele será redirecionado para a página inicial dizendo que ele não pode fazer isso.
É isso. Para mais informações ou para relatar um problema, visite a página do projeto no GitHub do Ryan.