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.
Agora executamos o seu generator para termos a nossa primeira policy definida.
Esta 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.
Que 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
.
Com 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.
Ou 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.
Sem 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.
Nã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.
Passamos apenas a classe dado que não estamos referenciando nenhum registro. Na listagem passamos o proprio objeto para o Pundit.
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.
Agora 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