Autorização no Rails com Pundit

Introdução

O Pundit é uma gem de autorização (ou seja, que garante que os recursos só sejam acessados por quem tem autorização). Ele faz isso criando classes de política de acesso para cada model que for autorizado e por padrão (mas não fixo) a classe terá como nome o model + “Policy”.

Para explicar pra vocês como utilizar a gem, vou utilizar um exemplo de uma locadora de filmes. Nela teremos um usuário que tem privilégios de administrador e outro não, onde o administrador pode gerenciar os filmes e o outro apenas criar uma locação e listar as locações feitas.


O que vamos aprender

– Como funciona o Pundit

– Como implementar autorizações no Rails

– Como definir os escopos de acesso


Ingredientes

– Ruby 2.4.1

– Rails 5.1

– Sqlite 3

– Pundit (gem)

– Devise (gem)


Objetivos

Apesar de estarmos utilizando a gem Devise para autenticação, o nosso foco é o Pundit. Queremos que você termine este post compreendendo melhor como utilizar esta ferramenta muito interessante para melhorar a autorização do seu app.

Vamos ao código \o/

 

Configurações iniciais do nosso app

  1. Primeiro, vamos gerar nosso projeto:

  1. Entre na pasta do projeto:

  1. Inclua as gems do Devise e do Pundit no seu Gemfile:

  1. Instale as gems:

  1. Instale o devise

  1. Vamos gerar um model User com o devise

  1. E executar nossos migrates


Agora que já temos o nosso model User criado, vamos criar alguns scaffolds que utilizaremos depois para colocarmos as autorizações

 

Configurando nossos Scaffolds

  1. Primeiro, vamos criar o scaffold para os filmes com atributos para o nome e outro para o tipo:

  1. Depois, vamos criar um outro scaffold para a locação com atributos para data da locação, o filme e o usuário que criou:

  1. Na partial de form do Rental que foi criado com o scaffold, precisamos colocar um select para os filmes (app/views/rentals/_form.html.erb).


a – Na linha onde foi gerado o seguinte código:

b – Substitua por:

c – E remova as linhas a baixo

  1. No controller RentalsController (app/controllers/rentals_controller.rb):


a – Na linha para filtrar os parâmetros, substitua o conteúdo a baixo:

b – Pelo conteúdo a seguir, para armazenarmos sempre o id do usuário que criou o aluguel:

  1. No arquivo config/routes.rb, adicione:

  1. Configure o ApplicationController para autenticar o usuário, adicionando a seguinte linha (app/controllers/application_controller.rb):

  1. Agora vamos criar um atributo para identificar se o usuário é ou não admin:

  1. E vamos executar os migrations


Agora vamos à parte principal do post! O PUNDIT \o/

Antes de tudo, nós temos que configurar o pundit no nosso app.

Primeiro, vamos criar um arquivo de política geral

Após executar este comando, foi criado com arquivo no caminho app/policies. O arquivo gerado é o ApplicationPolicy, ele segue o mesmo raciocínio do ApplicationController, é uma classe de onde todas as políticas vão herdar.

O conteúdo autogerado do meu arquivo, eu sobrescrevi por este, onde removi as ações padrão do Pundit (arquivo app/policies/application_policy.rb):

 

Repare: a política é uma classe pura do Ruby. Ela é inicializada com um registro de usuário (user) e um registro dos objetos que queremos autorizar (record). E como temos um attr_reader para estes dois registros, os métodos get destes atributos vão ficar disponíveis inclusive para a classe que herdar dela. A classe Scope interna a ela também é bem importante. Mas chegaremos lá =D.

As ações padrões que eu disse lá em cima são as seguintes: se você criar um arquivo de autorização para algum model e ele não tiver a autorização para uma determinada ação, ele vai buscar no ApplicationPolicy.

Por que eu removi as padrões? Para que a gente possa fazer um passo a passo mais claro sem nos preocuparmos com ele =)

 

Agora vamos seguir alguns passos para autorizar um recurso e já explico o que foi feito:

  1. Vamos incluir o Pundit nos controllers. Para isso nós vamos no ApplicationController e incluir a linha (app/controllers/application_controller.rb):

  1. Depois de incluir o Application Controller ficou desse jeito:

  1. Crie a classe para autorização aos filmes (model Movie):

(Perceba que ele criou um arquivo chamado MoviePolicy apenas com a classe Scope. Calma, vamos chegar na Scope.)

  1. Vamos no arquivo MoviePolicy (app/policies/movie_policy.rb) e incluir o seguinte método: (ele deve ficar dentro da classe MoviePolicy, mas fora da classe Scope)

  1. Apenas como amostra, o MoviePolicy ficou assim:

  1. Vamos no controller de filme, o MoviesController, e lá na action :index, vamos deixá-la da seguinte forma (app/controllers/movies_controller.rb):

 

O que foi feito nestes passos?

Depois que incluímos o Pundit no ApplicationController, foi criado um método chamado “index?” dentro do MoviePolicy. Este método é que vai executar a verificação de autorização conforme a operação lógica que está dentro dele.

Então, quando eu chamo “authorize Movie” na action :index do meu controller, eu estou dizendo: “Verifique na política do Movie se o acesso para a ação index está liberado”. Com isso, o método “index?” da política foi executado.

Mas, o método authorize ainda pode ser chamado de outras formas.
Como assim? Há quatro jeitos de chamar o authorize e vou usar o Movie como exemplo:

 

 

  • No primeiro authorize eu estou dizendo que será válido para qualquer filme (movie). Inclusive, quando eu chamo o “authorize” desta forma (com a classe), o registro (record) no arquivo Policy vem nulo. Ele pode ser utilizado quando você não precisa do objeto para autorizar.
  • No segundo eu estou especificando a ação que eu quero chamar no arquivo da política. Quando eu não especifico, ele considera a action atual do controller.
  • No terceiro eu estou especificando um objeto. É para quando é necessário fazer a autorização para um registro específico e nele o seu record vai ser preenchido para você poder construir as regras.
  • E no quarto, especificando a action e enviando um objeto, é o mesmo do segundo mas com o record da política preenchido.

 

Quando chamados este authorize, por baixos dos panos o Pundit está procurando quem é a classe de política do Movie, no caso a MoviePolicy, instanciando-a com o usuário, que por padrão ele pega o :current_user do controller, e com o registro (se for classe, ele instancia com nulo). Então, é chamado o método compatível com a ação.

 

Voltando ao código….

 

Eu sei que independente do filme, apenas o administrador vai poder acessar as ações. Então, para isso eu vou criar um hook before action no meu MovieController para ele sempre autorizar antes mesmo de fazer alguma operação:

 

  1. Crie o seguinte método em MoviesController (app/controllers/movies_controller.rb), de preferência como private:

  1. Adicione na primeira linha da sua classe:

  1. Remova o authorize da action :index, deixando ela desta forma:

  1. Vamos criar as autorizações para cada action de MoviesController em MoviePolicy, deixando o MoviePolicy da seguinte forma (app/policies/movie_policy.rb):

 

Mas, espera, e quando eu não tenho autorização a uma página e tento acessar? Bem, o Pundit retorna um erro: Pundit::NotAuthorizedError.

Com este erro, nós podemos adicionar mais um item em nosso ApplicationController para identificar este erro e, em vez de parar nossa aplicação, retornar o usuário para tela de filmes alugados. Para isso, adicione o seguinte código no ApplicationController (app/controllers/application_controller.rb):

 

 

Neste método “user_not_authorized” é possível retornar uma mensagem, setar uma flash message e várias outras coisas. Mas para simplificar eu vou apenas retornar o usuário para a tela de aluguéis.

 

Agora de volta aos aluguéis…

 

Vamos finalizar a parte de configurar as autorizações criando as políticas de acesso para as actions to Aluguel (Rental)

 

  1. Crie o arquivo de policy do Aluguel:

  1. Preencha o arquivo RentalPolicy (app/policies/rental_policy.rb) com as autorizações para as actions, deixando-o da seguinte forma:

  1. Crie um método privado em RentalsController para autorizar as ações (app/controllers/rentals_controller.rb):

  1. Configure os before_actions do RentalsController da seguinte forma:

 

O que fizemos nestes códigos foram:

  • Criar uma política de acesso para as actions de aluguel (Rental)
  • Criar um método para fazer a autorização do aluguel utilizando o objeto
  • Configurar os before_actions to controller para carregar o aluguel antes e depois, autorizá-lo.

 

Você deve ter percebido que eu não criei políticas para as ações index, new e create do aluguel. Bem fiz isso porque qualquer usuário pode criar um aluguel de um filme, mas somente quem criou o aluguel ou o administrador pode editar ou excluir. Foi justamente por isso que nas verificações da política, eu checo se o usuário é administrador ou é o dono do aluguel (Rental).

 

Vamos ver daqui há pouco que cada um só vai conseguir ver os aluguéis que criou.

 

Enfim, o escopo…

 

Com o pundit, nós podemos definir os escopos dos models. Hã? Bem, há situações, como a do aluguel, que eu não quero que o usuário veja os aluguéis do outro a não ser que ele seja o administrador.

E como podemos fazer isso? Simples, vamos utilizar o Scope que esta dentro do arquivo RentalPolicy.

 

Vamos colocar os seguintes trechos de codigo:

  1. No arquivo RentalPolicy, coloque o seguinte conteúdo na classe Scope (app/policies/rental_policy.rb):

  1. Substitua o método index do RentalsController, por este (app/controllers/rentals_controller.rb):

 

O que estamos fazendo neste trecho de código é: definindo um escopo que diz que se usuário for administrador, todos os aluguéis serão retornados e se ele não for, somente os aluguéis que ele é dono serão retornados. E na action :index do RentalsController eu estou dizendo: “use o escopo definido na policy de Aluguel (Rental)”.

Se visitarmos o sistema e criarmos uma novo usuário, veremos que todos ao aluguéis só serão exibidos para o usuário que tiver o atributo admin: true.

 

E para fechar…

 

Quero mostrar mais uma última e incrível feature que o Pundit nos fornece: meios de verificar a autorização no frontend.

 

Primeiro, abra o seu application.html.erb e adicione o seguinte trecho dentro do body antes do yield (app/views/layouts/application.html.erb)

 

Perceba que acabamos de adicionar 3 links: Um para ir para a lista de filmes, outro para ir para a lista de aluguéis e outro para Logout.

Mas pense comigo: Por que um usuário comum deveria ver o link “Movies” se é uma tela que ele não pode acessar? A resposta é: ele não deveria.

Poderíamos verificar se ele é admin no front-end? Poderíamos. Mas imagina o trabalho de, caso as políticas mudarem, ter quer ir no policy e alterar, depois sair procurando onde você estava checando se o usuário é admin… HARD.

 

Vamos usar o PUNDIT!

 

Coloque este código no seu body do application.html.erb (app/views/layouts/application.html.erb):

Ele disponibiliza um meio da gente chamar a política sem precisar ficar instanciando nada e ainda verifica se o usuário atual tem permissão para uma determinada ação.
Neste caso, primeiro eu verifico se o usuário está logado e depois chamo o método :index? da política MoviePolicy. É o mesmo método que usado para validar a action :index do MoviesController!

 

Onde eu coloquei:

Eu também poderia ter colocado, caso quisesse validar um objeto @movie específico em alguma view:

 

E o mais incrível, ele vai instanciar a classe exatamente do mesmo jeito que o authorize no controller: enviou uma classe, vai com record nulo, enviou objeto, vai record preenchido.

Aah.. e não esqueçam de acessar o seu console para poder colocar o seu usuário como admin caso você queira acessar as view de Movie. Afinal, neste exemplo nós não nos preocupamos com uma view voltada para o perfil do usuário para não desfocar.

 

Caso tenham alguma dúvida, aqui vai uma colinha =)

 

E dentro do console:

 

Conclusão

O Pundit é uma ferramenta que nos proporciona um meio de fazer autorizações no nosso app seguindo um padrão muito bem formado de arquivos de política utilizando convenção sobre configuração, além de proporcionar diversas facilidades para que possamos concentrar nossas permissões todas em apenas um arquivo de políticas do model.

É isso, galera!

O Post ficou grande, mas eu queria trazer pra vocês a grande ferramenta que o Pundit é e como ele pode ajudar nosso dia-a-dia de dev =D

 

Para mais detalhes, eu recomendo a leitura dos docs do Pundit:

https://github.com/elabs/pundit

E os trechos de códigos que foram colocados aqui foram tirados de um app de exemplo que construímos:

https://github.com/OneBitCodeBlog/movie_rental

 

E é isso, galera! Se precisarem de um help, só chamar! \o/

 

 


 

Aproveita e participar da Semana Super Full Stack onde vamos criar uma Rede Social do Zero \o/

 

dezembro 1, 2017

2 responses on "Autorização no Rails com Pundit"

  1. Legal conhecer este pundit. Eu costumo usar o CanCanCan com a classe Ability para definir a politica de Authorization, mas achei este modelo segmentado (em que uma classe “ClassPolicy” define as políticas) mais inteligível.

    Nas minhas aplicações maiores, o Ability.rb já está ficando insustentável.

Deixe uma resposta

Feito com s2 por OneBitCode
%d blogueiros gostam disto: