Pundit fournit un ensemble d'assistants qui vous guident dans l'exploitation des classes Ruby classiques et des modèles de conception orientés objet pour créer un système d'autorisation simple, robuste et évolutif.
Parrainé par : Varvet
Veuillez noter que le README sur GitHub est exact avec le dernier code sur GitHub . Vous utilisez probablement une version publiée de Pundit, veuillez donc vous référer à la documentation de la dernière version publiée de Pundit.
bundle add pundit
Incluez Pundit::Authorization
dans votre contrôleur d'application :
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
end
En option, vous pouvez exécuter le générateur, qui configurera une politique d'application avec quelques valeurs par défaut utiles pour vous :
rails g pundit:install
Après avoir généré votre politique d'application, redémarrez le serveur Rails afin que Rails puisse récupérer toutes les classes du nouveau répertoire app/policies/
.
Pundit se concentre sur la notion de classes politiques. Nous vous suggérons de placer ces classes dans app/policies
. Voici un exemple qui permet de mettre à jour une publication si l'utilisateur est un administrateur, ou si la publication n'est pas publiée :
class PostPolicy
attr_reader :user , :post
def initialize ( user , post )
@user = user
@post = post
end
def update?
user . admin? || ! post . published?
end
end
Comme vous pouvez le voir, il s’agit d’une simple classe Ruby. Pundit fait les hypothèses suivantes à propos de cette classe :
current_user
pour récupérer ce qu'il faut envoyer dans cet argumentupdate?
. Habituellement, cela correspondra au nom d’une action particulière du contrôleur.C'est vraiment ça.
Habituellement, vous souhaiterez hériter de la stratégie d'application créée par le générateur, ou configurer votre propre classe de base pour en hériter :
class PostPolicy < ApplicationPolicy
def update?
user . admin? or not record . published?
end
end
Dans l' ApplicationPolicy
générée, l'objet modèle est appelé record
.
En supposant que vous ayez une instance de la classe Post
, Pundit vous permet désormais de faire ceci dans votre contrôleur :
def update
@post = Post . find ( params [ :id ] )
authorize @post
if @post . update ( post_params )
redirect_to @post
else
render :edit
end
end
La méthode d'autorisation déduit automatiquement que Post
aura une classe PostPolicy
correspondante et instancie cette classe, en remettant l'utilisateur actuel et l'enregistrement donné. Il déduit ensuite du nom de l'action qu'il doit appeler update?
sur cette instance de la politique. Dans ce cas, vous pouvez imaginer que authorize
aurait fait quelque chose comme ceci :
unless PostPolicy . new ( current_user , @post ) . update?
raise Pundit :: NotAuthorizedError , "not allowed to PostPolicy#update? this Post"
end
Vous pouvez passer un deuxième argument pour authorize
si le nom de l'autorisation que vous souhaitez vérifier ne correspond pas au nom de l'action. Par exemple:
def publish
@post = Post . find ( params [ :id ] )
authorize @post , :update?
@post . publish!
redirect_to @post
end
Vous pouvez transmettre un argument pour remplacer la classe de stratégie si nécessaire. Par exemple:
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 vous n'avez pas d'instance pour le premier argument à authorize
, vous pouvez alors transmettre la classe. Par exemple:
Politique:
class PostPolicy < ApplicationPolicy
def admin_list?
user . admin?
end
end
Contrôleur:
def admin_list
authorize Post # we don't have a particular post to authorize
# Rest of controller action
end
authorize
renvoie l'instance qui lui est passée, vous pouvez donc la chaîner comme ceci :
Contrôleur:
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
Vous pouvez facilement mettre la main sur une instance de la stratégie via la méthode policy
à la fois dans la vue et dans le contrôleur. Ceci est particulièrement utile pour afficher de manière conditionnelle des liens ou des boutons dans la vue :
<% if policy(@post).update? %>
<%= link_to "Edit post", edit_post_path(@post) %>
<% end %>
Étant donné qu'il existe une politique sans modèle/classe Ruby correspondante, vous pouvez la récupérer en passant un symbole.
# 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
Notez que la politique sans tête doit toujours accepter deux arguments. Le deuxième argument sera le symbole :dashboard
dans ce cas, qui est ce qui est passé comme enregistrement à authorize
ci-dessous.
# In controllers
def show
authorize :dashboard , :show?
...
end
# In views
<% if policy(:dashboard).show? %>
<%= link_to 'Dashboard', dashboard_path %>
<% end %>
Souvent, vous souhaiterez disposer d’une sorte de vue répertoriant les enregistrements auxquels un utilisateur particulier a accès. Lorsque vous utilisez Pundit, vous devez définir une classe appelée portée de politique. Cela peut ressembler à ceci :
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 fait les hypothèses suivantes à propos de cette classe :
Scope
et est imbriquée sous la classe de stratégie.current_user
pour récupérer ce qu'il faut envoyer dans cet argument.ActiveRecord::Relation
, mais il pourrait s’agir de tout autre chose.resolve
, qui devrait renvoyer une sorte de résultat pouvant être itéré. Pour les classes ActiveRecord, il s’agirait généralement d’un ActiveRecord::Relation
.Vous souhaiterez probablement hériter de la portée de la stratégie d'application générée par le générateur, ou créer votre propre classe de base pour en hériter :
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
Vous pouvez désormais utiliser cette classe depuis votre contrôleur via la méthode policy_scope
:
def index
@posts = policy_scope ( Post )
end
def show
@post = policy_scope ( Post ) . find ( params [ :id ] )
end
Comme avec la méthode d'autorisation, vous pouvez également remplacer la classe de portée de la stratégie :
def index
# publication_class => Post
@publications = policy_scope ( publication_class , policy_scope_class : PublicationPolicy :: Scope )
end
Dans ce cas, il s'agit d'un raccourci pour faire :
def index
@publications = PublicationPolicy :: Scope . new ( current_user , Post ) . resolve
end
Vous pouvez, et êtes encouragés à, utiliser cette méthode dans les vues :
<% policy_scope(@user.posts).each do |post| %>
< p > <%= link_to post . title , post_path ( post ) %> </ p >
<% end %>
Lorsque vous développez une application avec Pundit, il peut être facile d'oublier d'autoriser certaines actions. Après tout, les gens oublient. Puisque Pundit vous encourage à ajouter manuellement l'appel authorize
à chaque action du contrôleur, il est très facile d'en manquer une.
Heureusement, Pundit dispose d'une fonctionnalité pratique qui vous rappelle en cas d'oubli. Pundit suit si vous avez appelé authorize
n'importe où dans l'action de votre contrôleur. Pundit ajoute également une méthode à vos contrôleurs appelée verify_authorized
. Cette méthode lèvera une exception si authorize
n'a pas encore été appelée. Vous devez exécuter cette méthode dans un hook after_action
pour vous assurer que vous n'avez pas oublié d'autoriser l'action. Par exemple:
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
after_action :verify_authorized
end
De même, Pundit ajoute également verify_policy_scoped
à votre contrôleur. Cela déclenchera une exception similaire à verify_authorized
. Cependant, il suit si policy_scope
est utilisé à la place de authorize
. Ceci est surtout utile pour les actions de contrôleur comme index
qui trouvent des collections avec une portée et n'autorisent pas les instances individuelles.
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
Ce mécanisme de vérification n'existe que pour vous aider lors du développement de votre application, n'oubliez donc pas d'appeler authorize
. Il ne s’agit pas d’une sorte de mécanisme de sécurité ou de mécanisme d’autorisation. Vous devriez pouvoir supprimer ces filtres sans affecter de quelque manière que ce soit le fonctionnement de votre application.
Certaines personnes ont trouvé cette fonctionnalité déroutante, tandis que beaucoup d’autres la trouvent extrêmement utile. Si vous appartenez à la catégorie des personnes qui trouvent cela déroutant, vous n’avez pas besoin de l’utiliser. Pundit fonctionnera correctement sans utiliser verify_authorized
et verify_policy_scoped
.
Si vous utilisez verify_authorized
dans vos contrôleurs mais que vous devez contourner la vérification de manière conditionnelle, vous pouvez utiliser skip_authorization
. Pour contourner verify_policy_scoped
, utilisez skip_policy_scope
. Celles-ci sont utiles dans les cas où vous ne souhaitez pas désactiver la vérification pour l'ensemble de l'action, mais dans certains cas où vous avez l'intention de ne pas autoriser.
def show
record = Record . find_by ( attribute : "value" )
if record . present?
authorize record
else
skip_authorization
end
end
Parfois, vous souhaiterez peut-être déclarer explicitement quelle politique utiliser pour une classe donnée, au lieu de laisser Pundit la déduire. Cela peut être fait comme ceci :
class Post
def self . policy_class
PostablePolicy
end
end
Alternativement, vous pouvez déclarer une méthode d'instance :
class Post
def policy_class
PostablePolicy
end
end
Pundit est volontairement une très petite bibliothèque, et elle ne fait rien que vous ne puissiez faire vous-même. Il n'y a pas de sauce secrète ici. Il en fait le moins possible, puis s'écarte de votre chemin.
Avec les quelques mais puissants assistants disponibles dans Pundit, vous avez le pouvoir de créer un système d'autorisation bien structuré et entièrement fonctionnel sans utiliser de DSL spécial ni de syntaxe géniale.
N'oubliez pas que toutes les classes de stratégie et de portée sont de simples classes Ruby, ce qui signifie que vous pouvez utiliser les mêmes mécanismes que vous utilisez toujours pour SÉCHER les choses. Encapsulez un ensemble d’autorisations dans un module et incluez-les dans plusieurs stratégies. Utilisez alias_method
pour que certaines autorisations se comportent de la même manière que d'autres. Héritez d'un ensemble d'autorisations de base. Utilisez la métaprogrammation si vous en avez vraiment besoin.
Utilisez le générateur fourni pour générer des politiques :
rails g pundit:policy post
Dans de nombreuses applications, seuls les utilisateurs connectés peuvent réellement faire quoi que ce soit. Si vous construisez un tel système, il peut être assez fastidieux de vérifier que l'utilisateur d'une stratégie n'est pas nil
pour chaque autorisation. Outre les politiques, vous pouvez ajouter cette vérification à la classe de base pour les étendues.
Nous vous suggérons de définir un filtre qui redirige les utilisateurs non authentifiés vers la page de connexion. En guise de défense secondaire, si vous avez défini une ApplicationPolicy, cela peut être une bonne idée de déclencher une exception si, d'une manière ou d'une autre, un utilisateur non authentifié réussit. De cette façon, vous pouvez échouer avec plus de grâce.
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
Pour prendre en charge un modèle d'objet nul, vous souhaiterez peut-être implémenter un NilClassPolicy
. Cela peut être utile lorsque vous souhaitez étendre votre ApplicationPolicy pour autoriser une certaine tolérance, par exemple, pour les associations qui peuvent être 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 génère une Pundit::NotAuthorizedError
que vous pouvez Rescue_from dans votre ApplicationController
. Vous pouvez personnaliser la méthode user_not_authorized
dans chaque contrôleur.
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
Alternativement, vous pouvez gérer globalement les Pundit::NotAuthorizedError en demandant aux rails de les gérer comme une erreur 403 et de diffuser une page d'erreur 403. Ajoutez ce qui suit à application.rb :
config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden
NotAuthorizedError
fournissent des informations sur quelle requête (par exemple :create?
), quel enregistrement (par exemple une instance de Post
) et quelle politique (par exemple une instance de PostPolicy
) a provoqué l'erreur.
Une façon d’utiliser ces propriétés query
, record
et policy
consiste à les connecter à I18n
pour générer des messages d’erreur. Voici comment procéder.
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! '
Ceci est un exemple. Pundit est indépendant de la manière dont vous implémentez votre message d'erreur.
Parfois, vous souhaitez récupérer une stratégie pour un enregistrement en dehors du contrôleur ou de la vue. Par exemple lorsque vous déléguez des autorisations d’une stratégie à une autre.
Vous pouvez facilement récupérer des politiques et des étendues comme ceci :
Pundit . policy! ( user , post )
Pundit . policy ( user , post )
Pundit . policy_scope! ( user , Post )
Pundit . policy_scope ( user , Post )
Les méthodes bang lèveront une exception si la politique n’existe pas, tandis que celles sans bang renverront zéro.
Parfois, votre contrôleur peut ne pas être en mesure d'accéder current_user
, ou la méthode qui doit être invoquée par Pundit peut ne pas être current_user
. Pour résoudre ce problème, vous pouvez définir une méthode dans votre contrôleur nommée pundit_user
.
def pundit_user
User . find_by_other_means
end
Dans certains cas, il peut être utile d'avoir plusieurs stratégies qui servent différents contextes pour une ressource. Un bon exemple de ceci est le cas où les stratégies utilisateur diffèrent des stratégies administrateur. Pour autoriser avec une stratégie d'espace de noms, transmettez l'espace de noms dans l'assistant authorize
dans un tableau :
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 vous utilisez des stratégies d'espace de noms pour quelque chose comme les vues d'administration, il peut être utile de remplacer policy_scope
et authorize
les assistants de votre AdminController
à appliquer automatiquement l'espace de noms :
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 vous encourage fortement à modéliser votre application de telle manière que le seul contexte dont vous avez besoin pour l'autorisation soit un objet utilisateur et un modèle de domaine pour lequel vous souhaitez vérifier l'autorisation. Si vous avez besoin de plus de contexte, demandez-vous si vous autorisez le bon modèle de domaine, peut-être qu'un autre modèle de domaine (ou un wrapper autour de plusieurs modèles de domaine) peut fournir le contexte dont vous avez besoin.
Pundit ne vous permet pas de transmettre des arguments supplémentaires aux politiques précisément pour cette raison.
Cependant, dans de très rares cas, vous devrez peut-être autoriser en fonction d'un contexte plus large que celui de l'utilisateur actuellement authentifié. Supposons par exemple que l'autorisation dépend de l'adresse IP en plus de l'utilisateur authentifié. Dans ce cas, une option consiste à créer une classe spéciale qui encapsule à la fois l'utilisateur et l'adresse IP et les transmet à la stratégie.
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
Dans Rails, la protection contre les affectations de masse est gérée dans le contrôleur. Avec Pundit, vous pouvez contrôler les attributs qu'un utilisateur a accès à la mise à jour via vos politiques. Vous pouvez configurer une méthode permitted_attributes
dans votre stratégie comme ceci :
# 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
Vous pouvez maintenant récupérer ces attributs de la stratégie :
# 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
Cependant, cela est un peu fastidieux, c'est pourquoi Pundit propose une méthode d'assistance pratique :
# 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 vous souhaitez autoriser différents attributs en fonction de l'action en cours, vous pouvez définir une méthode permitted_attributes_for_#{action}
sur votre stratégie :
# 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 vous avez défini une méthode spécifique à l'action sur votre stratégie pour l'action en cours, l'assistant permitted_attributes
l'appellera à la place.