Pundit proporciona un conjunto de ayudas que le guiarán en el aprovechamiento de las clases habituales de Ruby y los patrones de diseño orientados a objetos para crear un sistema de autorización sencillo, sólido y escalable.
Patrocinado por: Varvet
Tenga en cuenta que el archivo README de GitHub es exacto y tiene el código más reciente de GitHub . Lo más probable es que esté utilizando una versión publicada de Pundit, así que consulte la documentación para conocer la última versión publicada de Pundit.
bundle add pundit
Incluya Pundit::Authorization
en el controlador de su aplicación:
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
end
Opcionalmente, puede ejecutar el generador, que configurará una política de aplicación con algunos valores predeterminados útiles para usted:
rails g pundit:install
Después de generar la política de su aplicación, reinicie el servidor Rails para que Rails pueda seleccionar cualquier clase en el nuevo directorio app/policies/
.
Pundit se centra en la noción de clases de políticas. Le sugerimos que coloque estas clases en app/policies
. Este es un ejemplo que permite actualizar una publicación si el usuario es administrador o si la publicación no está 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 puede ver, esta es una clase Ruby simple. Pundit hace las siguientes suposiciones sobre esta clase:
current_user
para recuperar qué enviar en este argumento.update?
. Normalmente, esto se asignará al nombre de una acción del controlador en particular.Eso es todo realmente.
Por lo general, querrás heredar de la política de aplicación creada por el generador o configurar tu propia clase base para heredar de:
class PostPolicy < ApplicationPolicy
def update?
user . admin? or not record . published?
end
end
En la ApplicationPolicy
generada, el objeto modelo se llama record
.
Suponiendo que tienes una instancia de la clase Post
, Pundit ahora te permite hacer esto en tu controlador:
def update
@post = Post . find ( params [ :id ] )
authorize @post
if @post . update ( post_params )
redirect_to @post
else
render :edit
end
end
El método de autorización infiere automáticamente que Post
tendrá una clase PostPolicy
coincidente y crea una instancia de esta clase, entregando el usuario actual y el registro dado. Luego, del nombre de la acción se infiere que debería llamar update?
en esta instancia de la póliza. En este caso, puedes imaginar que authorize
habría hecho algo como esto:
unless PostPolicy . new ( current_user , @post ) . update?
raise Pundit :: NotAuthorizedError , "not allowed to PostPolicy#update? this Post"
end
Puede pasar un segundo argumento para authorize
si el nombre del permiso que desea verificar no coincide con el nombre de la acción. Por ejemplo:
def publish
@post = Post . find ( params [ :id ] )
authorize @post , :update?
@post . publish!
redirect_to @post
end
Puede pasar un argumento para anular la clase de política si es necesario. Por ejemplo:
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
Si no tiene una instancia para authorize
el primer argumento, puede pasar la clase. Por ejemplo:
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
devuelve la instancia que se le pasó, por lo que puedes encadenarla así:
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
Puede obtener fácilmente una instancia de la política a través del método policy
tanto en la vista como en el controlador. Esto es especialmente útil para mostrar enlaces o botones condicionalmente en la vista:
<% if policy(@post).update? %>
<%= link_to "Edit post", edit_post_path(@post) %>
<% end %>
Dado que hay una política sin un modelo/clase Ruby correspondiente, puede recuperarla pasando un 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
Tenga en cuenta que la política sin cabeza todavía necesita aceptar dos argumentos. El segundo argumento será el símbolo :dashboard
en este caso, que es lo que se pasa como registro para authorize
a continuación.
# In controllers
def show
authorize :dashboard , :show?
...
end
# In views
<% if policy(:dashboard).show? %>
<%= link_to 'Dashboard', dashboard_path %>
<% end %>
A menudo, querrás tener algún tipo de lista de registros a los que un usuario en particular tenga acceso. Al utilizar Pundit, se espera que defina una clase llamada alcance de política. Puede verse así:
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 hace las siguientes suposiciones sobre esta clase:
Scope
y está anidada en la clase de política.current_user
para recuperar qué enviar en este argumento.ActiveRecord::Relation
, pero podría ser algo completamente distinto.resolve
, que debería devolver algún tipo de resultado que pueda iterarse. Para las clases ActiveRecord, esto normalmente sería ActiveRecord::Relation
.Probablemente querrás heredar del alcance de la política de aplicación generado por el generador o crear tu propia clase base para heredar de:
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
Ahora puedes usar esta clase desde tu controlador a través del método policy_scope
:
def index
@posts = policy_scope ( Post )
end
def show
@post = policy_scope ( Post ) . find ( params [ :id ] )
end
Al igual que con el método de autorización, también puedes anular la clase de alcance de la política:
def index
# publication_class => Post
@publications = policy_scope ( publication_class , policy_scope_class : PublicationPolicy :: Scope )
end
En este caso es un atajo para hacer:
def index
@publications = PublicationPolicy :: Scope . new ( current_user , Post ) . resolve
end
Puede utilizar este método, y le recomendamos que lo haga, en las vistas:
<% policy_scope(@user.posts).each do |post| %>
< p > <%= link_to post . title , post_path ( post ) %> </ p >
<% end %>
Cuando desarrollas una aplicación con Pundit, puede ser fácil olvidarse de autorizar alguna acción. Después de todo, la gente es olvidadiza. Dado que Pundit lo alienta a agregar la llamada authorize
manualmente a cada acción del controlador, es muy fácil pasar por alto alguna.
Afortunadamente, Pundit tiene una función útil que te recuerda en caso de que lo olvides. Pundit rastrea si ha llamado authorize
en algún lugar de la acción de su controlador. Pundit también agrega un método a sus controladores llamado verify_authorized
. Este método generará una excepción si aún no se ha llamado authorize
. Debes ejecutar este método en un gancho after_action
para asegurarte de no haber olvidado autorizar la acción. Por ejemplo:
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
after_action :verify_authorized
end
Asimismo, Pundit también agrega verify_policy_scoped
a su controlador. Esto generará una excepción similar a verify_authorized
. Sin embargo, realiza un seguimiento si se utiliza policy_scope
en lugar de authorize
. Esto es especialmente útil para acciones del controlador como index
, que encuentra colecciones con un alcance y no autoriza instancias individuales.
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 verificación solo existe para ayudarlo mientras desarrolla su aplicación, por lo que no olvide llamar authorize
. No es algún tipo de mecanismo a prueba de fallos o mecanismo de autorización. Debería poder eliminar estos filtros sin afectar el funcionamiento de su aplicación de ninguna manera.
Algunas personas han encontrado esta función confusa, mientras que muchas otras la encuentran extremadamente útil. Si pertenece a la categoría de personas a las que les resulta confuso, no es necesario que lo utilice. Pundit funcionará bien sin usar verify_authorized
y verify_policy_scoped
.
Si está utilizando verify_authorized
en sus controladores pero necesita omitir la verificación condicionalmente, puede usar skip_authorization
. Para omitir verify_policy_scoped
, use skip_policy_scope
. Estos son útiles en circunstancias en las que no desea deshabilitar la verificación para toda la acción, pero en algunos casos desea no autorizar.
def show
record = Record . find_by ( attribute : "value" )
if record . present?
authorize record
else
skip_authorization
end
end
A veces es posible que desees declarar explícitamente qué política usar para una clase determinada, en lugar de dejar que Pundit la infiera. Esto se puede hacer así:
class Post
def self . policy_class
PostablePolicy
end
end
Alternativamente, puedes declarar un método de instancia:
class Post
def policy_class
PostablePolicy
end
end
Pundit es una biblioteca muy pequeña específicamente y no hace nada que no puedas hacer tú mismo. Aquí no hay ninguna salsa secreta. Hace lo menos posible y luego se aparta de tu camino.
Con los pocos pero poderosos ayudantes disponibles en Pundit, usted tiene el poder de construir un sistema de autorización bien estructurado y completamente funcional sin utilizar ningún DSL especial o sintaxis original.
Recuerde que todas las clases de política y alcance son clases simples de Ruby, lo que significa que puede usar los mismos mecanismos que siempre usa para SECAR las cosas. Encapsule un conjunto de permisos en un módulo e inclúyalos en múltiples políticas. Utilice alias_method
para hacer que algunos permisos se comporten igual que otros. Heredar de un conjunto básico de permisos. Utilice la metaprogramación si realmente es necesario.
Utilice el generador suministrado para generar políticas:
rails g pundit:policy post
En muchas aplicaciones, sólo los usuarios que han iniciado sesión pueden realmente hacer algo. Si está creando un sistema de este tipo, puede resultar un poco engorroso verificar que el usuario en una política no sea nil
para cada permiso. Aparte de las políticas, puede agregar esta verificación a la clase base para los ámbitos.
Le sugerimos que defina un filtro que redirija a los usuarios no autenticados a la página de inicio de sesión. Como defensa secundaria, si ha definido una ApplicationPolicy, podría ser una buena idea generar una excepción si de alguna manera un usuario no autenticado logra comunicarse. De esta manera podrás fallar con más gracia.
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 admitir un patrón de objeto nulo, es posible que desee implementar una NilClassPolicy
. Esto puede resultar útil cuando desee ampliar su ApplicationPolicy para permitir cierta tolerancia, por ejemplo, de asociaciones que podrían 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
Pundit genera un Pundit::NotAuthorizedError
que puedes rescatar en tu ApplicationController
. Puede personalizar el método user_not_authorized
en 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, puede manejar globalmente Pundit::NotAuthorizedError haciendo que Rails los maneje como un error 403 y sirviendo una página de error 403. Agregue lo siguiente a application.rb:
config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden
Los NotAuthorizedError
proporcionan información sobre qué consulta (por ejemplo :create?
), qué registro (por ejemplo, una instancia de Post
) y qué política (por ejemplo, una instancia de PostPolicy
) provocó que se generara el error.
Una forma de utilizar estas propiedades query
, record
y policy
es conectarlas con I18n
para generar mensajes de error. Así es como podrías hacerlo.
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 es un ejemplo. Pundit es independiente en cuanto a cómo implementa sus mensajes de error.
A veces desea recuperar una política para un registro fuera del controlador o la vista. Por ejemplo, cuando delega permisos de una política a otra.
Puede recuperar fácilmente políticas y alcances como este:
Pundit . policy! ( user , post )
Pundit . policy ( user , post )
Pundit . policy_scope! ( user , Post )
Pundit . policy_scope ( user , Post )
Los métodos bang generarán una excepción si la política no existe, mientras que aquellos sin bang devolverán cero.
En ocasiones, es posible que su controlador no pueda acceder current_user
o que el método que Pundit debería invocar no sea current_user
. Para solucionar esto, puede definir un método en su controlador llamado pundit_user
.
def pundit_user
User . find_by_other_means
end
En algunos casos, puede resultar útil tener varias políticas que sirvan a diferentes contextos para un recurso. Un excelente ejemplo de esto es el caso en el que las políticas de usuario difieren de las políticas de administrador. Para autorizar con una política de espacio de nombres, pase el espacio de nombres al asistente authorize
en una 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
Si está utilizando políticas de espacio de nombres para algo como vistas de administrador, puede resultar útil anular el policy_scope
y authorize
a los ayudantes en su AdminController
para que apliquen automáticamente el espacio de nombres:
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
Pundit le recomienda encarecidamente que modele su aplicación de tal manera que el único contexto que necesite para la autorización sea un objeto de usuario y un modelo de dominio para el que desee comprobar la autorización. Si necesita más contexto que eso, considere si está autorizando el modelo de dominio correcto; tal vez otro modelo de dominio (o un contenedor de múltiples modelos de dominio) pueda proporcionar el contexto que necesita.
Pundit no le permite pasar argumentos adicionales a las políticas precisamente por este motivo.
Sin embargo, en casos muy raros, es posible que necesites autorizar basándose en más contexto además del usuario actualmente autenticado. Supongamos, por ejemplo, que la autorización depende de la dirección IP además del usuario autenticado. En ese caso, una opción es crear una clase especial que incluya tanto al usuario como a la IP y los pase a la 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
En Rails, la protección de asignación masiva se maneja en el controlador. Con Pundit puedes controlar qué atributos tiene acceso un usuario para actualizar a través de tus políticas. Puede configurar un método permitted_attributes
en su política de esta manera:
# 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
Ahora puede recuperar estos atributos de la 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
Sin embargo, esto es un poco engorroso, por lo que Pundit proporciona un 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
Si desea permitir diferentes atributos según la acción actual, puede definir un método permitted_attributes_for_#{action}
en su 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
Si ha definido un método específico de acción en su política para la acción actual, el ayudante permitted_attributes
lo llamará en su lugar.