Autorização com Pundit
Criado em: 02/09/2014
Quem é da comunidade ruby há algum tempo já é acostumado com o CanCan e com o seu sucessor o CanCanCan que é a continuação do CanCan dado que o mesmo foi abandonado.
Em ambas das soluções temos apenas uma class Ability e definimos os nossos métodos de autorização ali. Para projetos pequenos pode ser uma boa saida, no entanto com o passar do tempo a complexidade aumenta e temos que extrair isso para classes expecificas e chama-las dentro da nossa interface do CanCan que é o model Ability. Mas e se já começassemos com uma abordagem mais inteligente? Com pequenas classes e cada uma definindo sua responsabilidade?
Olá Pundit
O Pundit utiliza o conceito de Policies ou Policy Objects para lidar com autorização. Mas antes de entrarmos a fundo no Pundit primeiro devemos ter um problema, então nosso problema é:
No sistema de gerenciamento de empresas temos 2 papeis o do gerente e o do empregado. O Gerente pode visualizar todas as telas do sistema. O empregado não pode criar, editar ou excluir nenhuma empresa.
Agora com o nosso problema em mão vamos iniciar. Não entrarei no mérito de autenticação, em que podemos utilizar o devise, nem na definição de papéis em que podemos utilizar o ActiveRecord::Enum.
Setup
Como de costume adicionamos ao nosso Gemfile gem "pundit". Depois de instalado incluimos o Pundit em nosso application controller.
class ApplicationController < ActionController::Base
include Pundit
# ...
endAgora executamos o seu generator para termos a nossa primeira policy definida.
$ rails g pundit:installEsta nossa primeira policy ainda não é utilizada ela será utilizada como base para as policies que formos definir. Ela define alguns padrões como por exemplo o destroy? como false, sendo assim se invocarmos um policy de destroy mas não o definirmos por padrão será false desde que herdermos de ApplicationPolicy.
Assim como o CanCan o Pundit utiliza do método current_user para pegar o usuário atual.
Criando nossa primeira Policy
Voltando lá ao nosso problema, devemos proteger as ações de criar, editar e excluir de empresa do usuário com o papel de empregado. Então vamos criar uma CompanyPolicy, para isso utilizamos do generator da seguinte maneira.
rails g pundit:policy companyQue nos gera uma classe para iniciarmos definindo a nossa policy. Então vamos definir primeiro que apenas o gerente pode criar uma nova empresa. Para isso definimos o método create?, por convenção o nome da action com um interrogação no final, e simplesmente utilizamos o método @user.manager? do User.
class CompanyPolicy < ApplicationPolicy
def create?
@user.manager?
end
endCom isso garantimos que apenas o usuário com o papel de gerente pode criar uma nova empresa. Mas você deve estar se perguntando, por que eu defini apenas o create? e não defini new? afinal não queremos que o funcionário acesse a tela de criar.
Isto ocorre por que no nosso ApplicationPolicy temos definidos estas regras da seguinte maneira.
def create?
false
end
def new?
create?
endOu seja new? executa o create? então devido ao poder da herança definimos apenas o create? em nossa classe filha. Este é um bom caso para o uso de herança, mas não se esqueça que composição tem um papel muito importante também quando estamos falando de orientação a objetos.
Em seguida definimos as demais policies da nossa company.
class CompanyPolicy < ApplicationPolicy
# ...
def update?
@user.manager?
end
def destroy?
@user.manager?
end
endSem nada de novo aqui.
Aplicando nossa policy
Agora com nossa policy criada vamos a nosso controller de company aplicá-la. Para isso simplesmente utilizamos do método authorize e passamos o objeto que estamos testando.
def new
@company = Company.new
authorize @company
endNão se esqueça de aplicar a policy para todas as actions que quer proteger.
Agora temos nossas actions protegidas, qualquer um que tentar acessar alguma dessas actions e não tiver permissão receberá uma excessão Pundit::NotAuthorizedError.
Protegemos o nosso controller, mas também não queremos links espalhados pelo sistema que levem a uma página de erro o ideal é esconde-los. Para isso vamos ao helper do pundit.
Aplicando nossa policy na view
O Pundit nos oferece o helper policy para utilizarmos na view para checarmos alguma policy. Para checarmos a exibição do link de criar por exemplo, fazemos assim.
- if policy(Company).new?
= link_to 'Nova empresa', new_company_pathPassamos apenas a classe dado que não estamos referenciando nenhum registro. Na listagem passamos o proprio objeto para o Pundit.
- if policy(company).update?
td = link_to 'Editar', edit_company_path(company)O legal do passarmos um objeto para dentro do Pundit, é que podemos por exemplo exibir o link de editar apenas para o gerente e em que a empresa tenha sido homologada por exemplo.
Por uma boa mensagem de erro
Até o momento caso algum usuário acesso alguma das actions em que ele não tem permissão ele recebe apenas uma excessão, o que se torna um erro 500 em produção, e não queremos isso. Então vamos tratar esta excessão para exibirmos uma boa mensagem de erro.
No nosso ApplicationController resgatamos da excessão e redirecionamos o usuário com uma mensagem de erro.
class ApplicationController < ActionController::Base
# ...
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
flash[:error] = 'Você não tem permissão para fazer esta ação'
redirect_to(request.referrer || root_path)
end
endAgora o usuário recebe uma melhor mensagem de erro.
Conclusão
O Pundit é uma boa alternativa ao CanCan, no entanto utilizando de objetos Ruby sem nenhuma DSL ou nada fancy. Não entrei no mérito dos testes para não alongar demais o post, mas o Pundit possui uma integração com o RSpec o que facilita bastante a nossa vida. Não deixe de experimentá-lo e tirar suas próprias conclusões.
Comentários
Comentários powered by Disqus