Pundit fornece um conjunto de ajudantes que orientam você no aproveitamento de classes Ruby regulares e padrões de design orientados a objetos para construir um sistema de autorização simples, robusto e escalável.
Patrocinado por: Varvet
Observe que o README no GitHub é compatível com o código mais recente no GitHub . Provavelmente você está usando uma versão lançada do Pundit, portanto consulte a documentação da versão mais recente do Pundit.
bundle add pundit
Inclua Pundit::Authorization
em seu controlador de aplicação:
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
end
Opcionalmente, você pode executar o gerador, que configurará uma política de aplicativo com alguns padrões úteis para você:
rails g pundit:install
Depois de gerar sua política de aplicação, reinicie o servidor Rails para que o Rails possa pegar qualquer classe no novo diretório app/policies/
.
O Pundit está focado na noção de classes políticas. Sugerimos que você coloque essas classes em app/policies
. Este é um exemplo que permite atualizar uma postagem se o usuário for administrador ou se a postagem não for publicada:
class PostPolicy
attr_reader :user , :post
def initialize ( user , post )
@user = user
@post = post
end
def update?
user . admin? || ! post . published?
end
end
Como você pode ver, esta é uma classe Ruby simples. Pundit faz as seguintes suposições sobre esta classe:
current_user
para recuperar o que enviar para este argumentoupdate?
. Normalmente, isso será mapeado para o nome de uma ação específica do controlador.É isso mesmo.
Normalmente você desejará herdar da política do aplicativo criada pelo gerador ou configurar sua própria classe base para herdar:
class PostPolicy < ApplicationPolicy
def update?
user . admin? or not record . published?
end
end
Na ApplicationPolicy
gerada, o objeto do modelo é chamado record
.
Supondo que você tenha uma instância da classe Post
, o Pundit agora permite fazer isso no seu controlador:
def update
@post = Post . find ( params [ :id ] )
authorize @post
if @post . update ( post_params )
redirect_to @post
else
render :edit
end
end
O método authorize infere automaticamente que Post
terá uma classe PostPolicy
correspondente e instancia essa classe, entregando o usuário atual e o registro fornecido. Em seguida, infere a partir do nome da ação que deveria chamar update?
nesta instância da política. Nesse caso, você pode imaginar que a authorize
teria feito algo assim:
unless PostPolicy . new ( current_user , @post ) . update?
raise Pundit :: NotAuthorizedError , "not allowed to PostPolicy#update? this Post"
end
Você pode passar um segundo argumento para authorize
se o nome da permissão que deseja verificar não corresponder ao nome da ação. Por exemplo:
def publish
@post = Post . find ( params [ :id ] )
authorize @post , :update?
@post . publish!
redirect_to @post
end
Você pode passar um argumento para substituir a classe de política, se necessário. Por exemplo:
def create
@publication = find_publication # assume this method returns any model that behaves like a publication
# @publication.class => Post
authorize @publication , policy_class : PublicationPolicy
@publication . publish!
redirect_to @publication
end
Se você não tiver uma instância para o primeiro argumento de authorize
, poderá passar a classe. Por exemplo:
Política:
class PostPolicy < ApplicationPolicy
def admin_list?
user . admin?
end
end
Controlador:
def admin_list
authorize Post # we don't have a particular post to authorize
# Rest of controller action
end
authorize
retorna a instância passada para ele, então você pode encadeá-la assim:
Controlador:
def show
@user = authorize User . find ( params [ :id ] )
end
# return the record even for namespaced policies
def show
@user = authorize [ :admin , User . find ( params [ :id ] ) ]
end
Você pode facilmente obter uma instância da política por meio do método policy
na visualização e no controlador. Isso é especialmente útil para mostrar condicionalmente links ou botões na visualização:
<% if policy(@post).update? %>
<%= link_to "Edit post", edit_post_path(@post) %>
<% end %>
Dado que existe uma política sem um modelo/classe Ruby correspondente, você pode recuperá-la passando um símbolo.
# app/policies/dashboard_policy.rb
class DashboardPolicy
attr_reader :user
# `_record` in this example will be :dashboard
def initialize ( user , _record )
@user = user
end
def show?
user . admin?
end
end
Observe que a política headless ainda precisa aceitar dois argumentos. O segundo argumento será o símbolo :dashboard
neste caso, que é passado como registro para authorize
abaixo.
# In controllers
def show
authorize :dashboard , :show?
...
end
# In views
<% if policy(:dashboard).show? %>
<%= link_to 'Dashboard', dashboard_path %>
<% end %>
Freqüentemente, você desejará ter algum tipo de visualização de registros aos quais um determinado usuário tenha acesso. Ao usar o Pundit, espera-se que você defina uma classe chamada escopo de política. Pode ser algo assim:
class PostPolicy < ApplicationPolicy
class Scope
def initialize ( user , scope )
@user = user
@scope = scope
end
def resolve
if user . admin?
scope . all
else
scope . where ( published : true )
end
end
private
attr_reader :user , :scope
end
def update?
user . admin? or not record . published?
end
end
Pundit faz as seguintes suposições sobre esta classe:
Scope
e está aninhada na classe de política.current_user
para recuperar o que enviar para este argumento.ActiveRecord::Relation
, mas pode ser algo totalmente diferente.resolve
, que deve retornar algum tipo de resultado que possa ser iterado. Para classes ActiveRecord, geralmente seria um ActiveRecord::Relation
.Você provavelmente desejará herdar do escopo da política de aplicativo gerado pelo gerador ou criar sua própria classe base para herdar:
class PostPolicy < ApplicationPolicy
class Scope < ApplicationPolicy :: Scope
def resolve
if user . admin?
scope . all
else
scope . where ( published : true )
end
end
end
def update?
user . admin? or not record . published?
end
end
Agora você pode usar esta classe do seu controlador através do método policy_scope
:
def index
@posts = policy_scope ( Post )
end
def show
@post = policy_scope ( Post ) . find ( params [ :id ] )
end
Assim como acontece com o método authorize, você também pode substituir a classe de escopo da política:
def index
# publication_class => Post
@publications = policy_scope ( publication_class , policy_scope_class : PublicationPolicy :: Scope )
end
Neste caso é um atalho para fazer:
def index
@publications = PublicationPolicy :: Scope . new ( current_user , Post ) . resolve
end
Você pode, e é incentivado a, usar este método nas visualizações:
<% policy_scope(@user.posts).each do |post| %>
< p > <%= link_to post . title , post_path ( post ) %> </ p >
<% end %>
Ao desenvolver um aplicativo com o Pundit, pode ser fácil esquecer de autorizar alguma ação. Afinal, as pessoas são esquecidas. Como o Pundit incentiva você a adicionar manualmente a chamada authorize
a cada ação do controlador, é muito fácil perder uma.
Felizmente, o Pundit tem um recurso útil que o lembra caso você esqueça. O Pundit rastreia se você chamou authorize
em qualquer lugar da ação do controlador. O Pundit também adiciona um método aos seus controladores chamado verify_authorized
. Este método irá gerar uma exceção se authorize
ainda não tiver sido chamada. Você deve executar este método em um gancho after_action
para garantir que não se esqueceu de autorizar a ação. Por exemplo:
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
after_action :verify_authorized
end
Da mesma forma, o Pundit também adiciona verify_policy_scoped
ao seu controlador. Isso gerará uma exceção semelhante a verify_authorized
. No entanto, ele rastreia se policy_scope
é usado em vez de authorize
. Isso é útil principalmente para ações de controlador, como index
, que encontram coleções com escopo e não autorizam instâncias individuais.
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
after_action :verify_pundit_authorization
def verify_pundit_authorization
if action_name == "index"
verify_policy_scoped
else
verify_authorized
end
end
end
Este mecanismo de verificação existe apenas para auxiliá-lo no desenvolvimento de sua aplicação, para que você não se esqueça de chamar authorize
. Não é algum tipo de mecanismo à prova de falhas ou mecanismo de autorização. Você deve ser capaz de remover esses filtros sem afetar de forma alguma o funcionamento do seu aplicativo.
Algumas pessoas acharam esse recurso confuso, enquanto muitas outras o consideram extremamente útil. Se você se enquadra na categoria de pessoas que acham isso confuso, não precisa usá-lo. O Pundit funcionará bem sem usar verify_authorized
e verify_policy_scoped
.
Se você estiver usando verify_authorized
em seus controladores, mas precisar ignorar condicionalmente a verificação, poderá usar skip_authorization
. Para ignorar verify_policy_scoped
, use skip_policy_scope
. Eles são úteis em circunstâncias em que você não deseja desabilitar a verificação para toda a ação, mas há alguns casos em que você pretende não autorizar.
def show
record = Record . find_by ( attribute : "value" )
if record . present?
authorize record
else
skip_authorization
end
end
Às vezes você pode querer declarar explicitamente qual política usar para uma determinada classe, em vez de deixar o Pundit inferir isso. Isso pode ser feito assim:
class Post
def self . policy_class
PostablePolicy
end
end
Alternativamente, você pode declarar um método de instância:
class Post
def policy_class
PostablePolicy
end
end
Pundit é propositalmente uma biblioteca muito pequena e não faz nada que você não possa fazer sozinho. Não há molho secreto aqui. Ele faz o mínimo possível e depois sai do seu caminho.
Com os poucos, mas poderosos, ajudantes disponíveis no Pundit, você tem o poder de construir um sistema de autorização bem estruturado e totalmente funcional, sem usar nenhuma DSL especial ou sintaxe estranha.
Lembre-se de que todas as classes de política e escopo são classes Ruby simples, o que significa que você pode usar os mesmos mecanismos que sempre usa para secar as coisas. Encapsule um conjunto de permissões em um módulo e inclua-as em diversas políticas. Use alias_method
para fazer com que algumas permissões se comportem da mesma forma que outras. Herdar de um conjunto básico de permissões. Use metaprogramação se realmente for necessário.
Use o gerador fornecido para gerar políticas:
rails g pundit:policy post
Em muitos aplicativos, apenas usuários logados são realmente capazes de fazer qualquer coisa. Se você estiver construindo um sistema desse tipo, pode ser um pouco complicado verificar se o usuário em uma política não é nil
para cada permissão. Além das políticas, você pode adicionar essa verificação à classe base dos escopos.
Sugerimos que você defina um filtro que redirecione usuários não autenticados para a página de login. Como defesa secundária, se você definiu uma ApplicationPolicy, pode ser uma boa ideia gerar uma exceção se, de alguma forma, um usuário não autenticado conseguir passar. Dessa forma, você pode falhar com mais elegância.
class ApplicationPolicy
def initialize ( user , record )
raise Pundit :: NotAuthorizedError , "must be logged in" unless user
@user = user
@record = record
end
class Scope
attr_reader :user , :scope
def initialize ( user , scope )
raise Pundit :: NotAuthorizedError , "must be logged in" unless user
@user = user
@scope = scope
end
end
end
Para oferecer suporte a um padrão de objeto nulo, você pode querer implementar um NilClassPolicy
. Isso pode ser útil quando você deseja estender sua ApplicationPolicy para permitir alguma tolerância, por exemplo, de associações que podem ser nil
.
class NilClassPolicy < ApplicationPolicy
class Scope < ApplicationPolicy :: Scope
def resolve
raise Pundit :: NotDefinedError , "Cannot scope NilClass"
end
end
def show?
false # Nobody can see nothing
end
end
O Pundit gera um Pundit::NotAuthorizedError
que você pode Rescue_from no seu ApplicationController
. Você pode personalizar o método user_not_authorized
em cada controlador.
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
rescue_from Pundit :: NotAuthorizedError , with : :user_not_authorized
private
def user_not_authorized
flash [ :alert ] = "You are not authorized to perform this action."
redirect_back_or_to ( root_path )
end
end
Alternativamente, você pode lidar globalmente com Pundit::NotAuthorizedError fazendo com que o Rails os trate como um erro 403 e exibindo uma página de erro 403. Adicione o seguinte ao application.rb:
config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden
NotAuthorizedError
s fornecem informações sobre qual consulta (por exemplo :create?
), qual registro (por exemplo, uma instância de Post
) e qual política (por exemplo, uma instância de PostPolicy
) causou o erro a ser gerado.
Uma maneira de usar essas propriedades query
, record
e policy
é conectá-las ao I18n
para gerar mensagens de erro. Veja como você pode fazer isso.
class ApplicationController < ActionController :: Base
rescue_from Pundit :: NotAuthorizedError , with : :user_not_authorized
private
def user_not_authorized ( exception )
policy_name = exception . policy . class . to_s . underscore
flash [ :error ] = t " #{ policy_name } . #{ exception . query } " , scope : "pundit" , default : :default
redirect_back_or_to ( root_path )
end
end
en :
pundit :
default : ' You cannot perform this action. '
post_policy :
update? : ' You cannot edit this post! '
create? : ' You cannot create posts! '
Este é um exemplo. O Pundit é agnóstico quanto à forma como você implementa suas mensagens de erro.
Às vezes você deseja recuperar uma política para um registro fora do controlador ou da visualização. Por exemplo, quando você delega permissões de uma política para outra.
Você pode recuperar facilmente políticas e escopos como este:
Pundit . policy! ( user , post )
Pundit . policy ( user , post )
Pundit . policy_scope! ( user , Post )
Pundit . policy_scope ( user , Post )
Os métodos bang gerarão uma exceção se a política não existir, enquanto aqueles sem bang retornarão nulo.
Ocasionalmente, seu controlador pode não conseguir acessar current_user
, ou o método que deve ser invocado pelo Pundit pode não ser current_user
. Para resolver isso, você pode definir um método em seu controlador chamado pundit_user
.
def pundit_user
User . find_by_other_means
end
Em alguns casos, pode ser útil ter múltiplas políticas que sirvam diferentes contextos para um recurso. Um excelente exemplo disso é o caso em que as políticas do usuário diferem das políticas do administrador. Para autorizar com uma política com namespace, passe o namespace para o auxiliar authorize
em uma matriz:
authorize ( post ) # => will look for a PostPolicy
authorize ( [ :admin , post ] ) # => will look for an Admin::PostPolicy
authorize ( [ :foo , :bar , post ] ) # => will look for a Foo::Bar::PostPolicy
policy_scope ( Post ) # => will look for a PostPolicy::Scope
policy_scope ( [ :admin , Post ] ) # => will look for an Admin::PostPolicy::Scope
policy_scope ( [ :foo , :bar , Post ] ) # => will look for a Foo::Bar::PostPolicy::Scope
Se você estiver usando políticas com namespace para algo como visualizações Admin, pode ser útil substituir o policy_scope
e authorize
auxiliares em seu AdminController
a aplicar automaticamente o namespace:
class AdminController < ApplicationController
def policy_scope ( scope )
super ( [ :admin , scope ] )
end
def authorize ( record , query = nil )
super ( [ :admin , record ] , query )
end
end
class Admin :: PostController < AdminController
def index
policy_scope ( Post )
end
def show
post = authorize Post . find ( params [ :id ] )
end
end
O Pundit recomenda fortemente que você modele seu aplicativo de tal forma que o único contexto necessário para autorização seja um objeto de usuário e um modelo de domínio para o qual você deseja verificar a autorização. Se você precisar de mais contexto do que isso, considere se está autorizando o modelo de domínio correto, talvez outro modelo de domínio (ou um wrapper em torno de vários modelos de domínio) possa fornecer o contexto que você precisa.
O Pundit não permite que você transmita argumentos adicionais às políticas precisamente por esse motivo.
No entanto, em casos muito raros, pode ser necessário autorizar com base em mais contexto do que apenas o usuário atualmente autenticado. Suponha, por exemplo, que a autorização dependa do endereço IP além do usuário autenticado. Nesse caso, uma opção é criar uma classe especial que agrupe o usuário e o IP e os passe para a política.
class UserContext
attr_reader :user , :ip
def initialize ( user , ip )
@user = user
@ip = ip
end
end
class ApplicationController
include Pundit :: Authorization
def pundit_user
UserContext . new ( current_user , request . ip )
end
end
No Rails, a proteção de atribuição de massa é tratada no controlador. Com o Pundit você pode controlar quais atributos um usuário tem acesso para atualizar por meio de suas políticas. Você pode configurar um método permitted_attributes
em sua política assim:
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def permitted_attributes
if user . admin? || user . owner_of? ( post )
[ :title , :body , :tag_list ]
else
[ :tag_list ]
end
end
end
Agora você pode recuperar estes atributos da política:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def update
@post = Post . find ( params [ :id ] )
if @post . update ( post_params )
redirect_to @post
else
render :edit
end
end
private
def post_params
params . require ( :post ) . permit ( policy ( @post ) . permitted_attributes )
end
end
No entanto, isso é um pouco complicado, então o Pundit fornece um método auxiliar conveniente:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def update
@post = Post . find ( params [ :id ] )
if @post . update ( permitted_attributes ( @post ) )
redirect_to @post
else
render :edit
end
end
end
Se quiser permitir atributos diferentes com base na ação atual, você pode definir um método permitted_attributes_for_#{action}
em sua política:
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def permitted_attributes_for_create
[ :title , :body ]
end
def permitted_attributes_for_edit
[ :body ]
end
end
Se você definiu um método específico de ação em sua política para a ação atual, o auxiliar permitted_attributes
irá chamá-lo em vez disso.