
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
- Primeiro, vamos gerar nosso projeto:
1 |
rails new movie_rental |
- Entre na pasta do projeto:
1 |
cd movie_rental |
- Inclua as gems do Devise e do Pundit no seu Gemfile:
1 2 |
gem 'devise' gem 'pundit' |
- Instale as gems:
1 |
bundle |
- Instale o devise
1 |
rails g devise:install |
- Vamos gerar um model User com o devise
1 |
rails g devise User |
- E executar nossos migrates
1 |
rails db:migrate |
Agora que já temos o nosso model User criado, vamos criar alguns scaffolds que utilizaremos depois para colocarmos as autorizações
Configurando nossos Scaffolds
- Primeiro, vamos criar o scaffold para os filmes com atributos para o nome e outro para o tipo:
1 |
rails g scaffold movie name kind |
- 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 |
rails g scaffold rental date:datetime movie:references user:references |
- 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:
1 |
<%= form.text_field :movie_id, id: :rental_movie_id %> |
b – Substitua por:
1 |
<%= form.select :movie_id, Movie.all.collect {|m| [ m.name, m.id ] }, id: :rental_movie_id %> |
c – E remova as linhas a baixo
1 2 3 4 |
<div class="field"> <%= form.label :user_id %> <%= form.text_field :user_id, id: :rental_user_id %> </div> |
- No controller RentalsController (app/controllers/rentals_controller.rb):
a – Na linha para filtrar os parâmetros, substitua o conteúdo a baixo:
1 2 3 |
def rental_params params.require(:rental).permit(:date, :movie_id) end |
b – Pelo conteúdo a seguir, para armazenarmos sempre o id do usuário que criou o aluguel:
1 2 3 |
def rental_params params.require(:rental).permit(:date, :movie_id).merge(user_id: current_user.id) end |
- No arquivo config/routes.rb, adicione:
1 |
root to: "rentals#index" |
- Configure o ApplicationController para autenticar o usuário, adicionando a seguinte linha (app/controllers/application_controller.rb):
1 |
before_action :authenticate_user! |
- Agora vamos criar um atributo para identificar se o usuário é ou não admin:
1 |
rails g migration add_admin_to_users admin:boolean |
- E vamos executar os migrations
1 |
rails db:migrate |
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
1 |
rails g pundit:install |
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):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class ApplicationPolicy attr_reader :user, :record def initialize(user, record) @user = user @record = record end class Scope attr_reader :user, :scope def initialize(user, scope) @user = user @scope = scope end def resolve scope end end end |
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:
- Vamos incluir o Pundit nos controllers. Para isso nós vamos no ApplicationController e incluir a linha (app/controllers/application_controller.rb):
1 |
include Pundit |
- Depois de incluir o Application Controller ficou desse jeito:
1 2 3 4 5 6 |
class ApplicationController < ActionController::Base include Pundit protect_from_forgery with: :exception before_action :authenticate_user! end |
- Crie a classe para autorização aos filmes (model Movie):
1 |
rails g pundit:policy movie |
(Perceba que ele criou um arquivo chamado MoviePolicy apenas com a classe Scope. Calma, vamos chegar na Scope.)
- 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 2 3 |
def index? user.admin? end |
- Apenas como amostra, o MoviePolicy ficou assim:
1 2 3 4 5 6 7 8 9 10 11 |
class MoviePolicy < ApplicationPolicy def index? user.admin? end class Scope < Scope def resolve scope end end end |
- Vamos no controller de filme, o MoviesController, e lá na action :index, vamos deixá-la da seguinte forma (app/controllers/movies_controller.rb):
1 2 3 4 |
def index authorize Movie @movies = Movie.all end |
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:
1 2 3 4 |
authorize Movie authorize Movie, :any_action? authotize @movie authorize @movie, :any_action? |
- 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:
- Crie o seguinte método em MoviesController (app/controllers/movies_controller.rb), de preferência como private:
1 2 3 |
def authorize_movie authorize Movie end |
- Adicione na primeira linha da sua classe:
1 |
before_action :authorize_movie |
- Remova o authorize da action :index, deixando ela desta forma:
1 2 3 |
def index @movies = Movie.all end |
- Vamos criar as autorizações para cada action de MoviesController em MoviePolicy, deixando o MoviePolicy da seguinte forma (app/policies/movie_policy.rb):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class MoviePolicy < ApplicationPolicy class Scope < Scope def resolve scope end end def index? user.admin? end def new? user.admin? end def create? user.admin? end def show? user.admin? end def edit? user.admin? end def update? user.admin? end def destroy? user.admin? end end |
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):
1 2 3 4 5 6 7 |
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized private def user_not_authorized redirect_to root_path end |
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)
- Crie o arquivo de policy do Aluguel:
1 |
rails g pundit:policy rental |
- Preencha o arquivo RentalPolicy (app/policies/rental_policy.rb) com as autorizações para as actions, deixando-o da seguinte forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class RentalPolicy < ApplicationPolicy class Scope < Scope def resolve scope end end def show? user.admin? || record.user == user end def edit? user.admin? || record.user == user end def update? user.admin? || record.user == user end def destroy? user.admin? || record.user == user end end |
- Crie um método privado em RentalsController para autorizar as ações (app/controllers/rentals_controller.rb):
1 2 3 |
def authorize_rental authorize @rental end |
- Configure os before_actions do RentalsController da seguinte forma:
1 2 3 4 |
class RentalsController < ApplicationController before_action :set_rental, only: [:show, :edit, :update, :destroy] before_action :authorize_rental, only: [:show, :edit, :update, :destroy] … end |
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:
- No arquivo RentalPolicy, coloque o seguinte conteúdo na classe Scope (app/policies/rental_policy.rb):
1 2 3 4 5 6 7 8 9 |
class Scope < Scope def resolve if user.admin? scope.all else scope.where(user: user) end end end |
- Substitua o método index do RentalsController, por este (app/controllers/rentals_controller.rb):
1 2 3 |
def index @rentals = policy_scope(Rental).all end |
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)
1 2 3 4 |
<%= link_to "Movies", movies_path %> <%= link_to "Rental", rentals_path %> <%= link_to "Logout", destroy_user_session_path, method: :delete %> <%= yield %> |
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):
1 2 3 4 5 6 7 |
<% if user_signed_in? %> <% if policy(Movie).index? %> <%= link_to "Movies", movies_path %> <% end %> <%= link_to "Rental", rentals_path %> <%= link_to "Logout", destroy_user_session_path, method: :delete %> <% end %> |
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:
1 |
policy(Movie).index? |
Eu também poderia ter colocado, caso quisesse validar um objeto @movie específico em alguma view:
1 |
policy(@movie).index? |
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 =)
1 |
rails c |
E dentro do console:
1 |
User.find_by(email: ).update(admin: true) |

Não perca nenhum conteúdo
Receba nosso resumo semanal com os novos posts, cursos, talks e vagas o/
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/
Primeira vez no OneBitCode? Curtiu esse conteúdo?
O OneBitCode tem muito mais para você!
O OneBitCode traz conteúdos de qualidade, e em português, sobre programação com foco em Ruby on Rails e também JavaScript.
Além disso, aqui sempre levamos à você conteúdos valiosos sobre a carreira de programação, dicas sobre currículos, portfólios, perfil profissional, soft skills, enfim, tudo o que você precisa saber para continuar evoluindo como Programador(a)!
Fique por dentro de todos os conteúdos o/
Nossas redes sociais:
📹 • https://youtube.com/Onebitcode [Live todas as terças-feiras às 19h)
💻 • https://linkedin.com/company/onebitcode
🙂 • https://facebook.com/onebitcode
📱 • https://instagram.com/one_bit_code
🐦 • https://twitter.com/onebitcode
Nossos cursos:
🥇 • Programador Full Stack Javascript em 8 Semanas
💎 • https://onebitcode.com/free-speed-dating-chicago/
⚙ • difference between dating and relationship
🐞 • Minicurso de Testes para Ruby on Rails com RSpec
Espero que curta nossos conteúdos e sempre que precisar de ajuda, fala com a gente!
Estamos aqui para você 🙂
Bem-vindo à família OneBitCode o/
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.
Legal Donato, bora testar 🙂
Muito bom tutorial, mas acho que tem que tirar o end desse método:
def authorize_rental
authorize @rentalend
end
e mudar para:
def authorize_rental
authorize @rental
end
E a cara beleza?
Valeu por acompanhar o Blog e por fazer esse report (ajustamos)
Abraço
Dá pra usar o Pundit quando tem múltiplas regras de usuários, como: admin, manager, user ?
Ei, Gláucia. Dá sim, sem problemas. No arquivo de política, na parte do Scope, vc pode colocar ifs e elses (ou case when) para pegar pegar o escopo de cada uma. O mesmo vale para as actions no arquivo de política, vc pode verificar se o usuário é admin, manager ou qualquer outro profile.
Muito bom!
Imaginei um cenário e não identifiquei onde colocaria:
Cenário:
Dependente esta autorizado ao locar filme em nome do titular.
Exemplo filho que aluga o filme na conta do pai.
Teria que fazer algumas checagem.
filho pode locar?
filho pode locar em nome de alguém?
este alguém autorizou este dependente?