Pundit bietet eine Reihe von Hilfsprogrammen, die Sie dabei unterstützen, reguläre Ruby-Klassen und objektorientierte Entwurfsmuster zu nutzen, um ein unkompliziertes, robustes und skalierbares Autorisierungssystem aufzubauen.
Gesponsert von: Varvet
Bitte beachten Sie , dass die README-Datei auf GitHub mit dem neuesten Code auf GitHub übereinstimmt. Sie verwenden höchstwahrscheinlich eine veröffentlichte Version von Pundit. Sehen Sie sich daher bitte die Dokumentation für die neueste veröffentlichte Version von Pundit an.
bundle add pundit
Fügen Sie Pundit::Authorization
in Ihren Anwendungscontroller ein:
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
end
Optional können Sie den Generator ausführen, der eine Anwendungsrichtlinie mit einigen nützlichen Standardeinstellungen für Sie einrichtet:
rails g pundit:install
Starten Sie nach dem Generieren Ihrer Anwendungsrichtlinie den Rails-Server neu, damit Rails alle Klassen im neuen Verzeichnis app/policies/
abrufen kann.
Pundit konzentriert sich auf den Begriff der Versicherungsklassen. Wir empfehlen, dass Sie diese Klassen in app/policies
einfügen. Dies ist ein Beispiel, das die Aktualisierung eines Beitrags ermöglicht, wenn der Benutzer ein Administrator ist oder der Beitrag unveröffentlicht ist:
class PostPolicy
attr_reader :user , :post
def initialize ( user , post )
@user = user
@post = post
end
def update?
user . admin? || ! post . published?
end
end
Wie Sie sehen, handelt es sich hierbei um eine einfache Ruby-Klasse. Pundit macht die folgenden Annahmen über diese Klasse:
current_user
auf, um abzurufen, was an dieses Argument gesendet werden sollupdate?
. Normalerweise wird dies dem Namen einer bestimmten Controller-Aktion zugeordnet.Das ist es wirklich.
Normalerweise möchten Sie von der vom Generator erstellten Anwendungsrichtlinie erben oder Ihre eigene Basisklasse einrichten, um von Folgendem zu erben:
class PostPolicy < ApplicationPolicy
def update?
user . admin? or not record . published?
end
end
In der generierten ApplicationPolicy
heißt das Modellobjekt record
.
Angenommen, Sie haben eine Instanz der Klasse Post
, können Sie mit Pundit nun Folgendes in Ihrem Controller tun:
def update
@post = Post . find ( params [ :id ] )
authorize @post
if @post . update ( post_params )
redirect_to @post
else
render :edit
end
end
Die Autorisierungsmethode leitet automatisch ab, dass Post
über eine passende PostPolicy
Klasse verfügt, instanziiert diese Klasse und übergibt den aktuellen Benutzer und den angegebenen Datensatz. Aus dem Aktionsnamen wird dann abgeleitet, dass update?
auf dieser Instanz der Richtlinie. In diesem Fall können Sie sich vorstellen, dass authorize
etwa Folgendes getan hätte:
unless PostPolicy . new ( current_user , @post ) . update?
raise Pundit :: NotAuthorizedError , "not allowed to PostPolicy#update? this Post"
end
Sie können ein zweites Argument zur authorize
übergeben, wenn der Name der Berechtigung, die Sie überprüfen möchten, nicht mit dem Aktionsnamen übereinstimmt. Zum Beispiel:
def publish
@post = Post . find ( params [ :id ] )
authorize @post , :update?
@post . publish!
redirect_to @post
end
Sie können bei Bedarf ein Argument übergeben, um die Richtlinienklasse zu überschreiben. Zum Beispiel:
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
Wenn Sie keine Instanz für das erste Argument zur authorize
haben, können Sie die Klasse übergeben. Zum Beispiel:
Politik:
class PostPolicy < ApplicationPolicy
def admin_list?
user . admin?
end
end
Regler:
def admin_list
authorize Post # we don't have a particular post to authorize
# Rest of controller action
end
authorize
gibt die an ihn übergebene Instanz zurück, sodass Sie sie wie folgt verketten können:
Regler:
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
Über die policy
in der Ansicht und im Controller können Sie problemlos auf eine Instanz der Richtlinie zugreifen. Dies ist besonders nützlich, um Links oder Schaltflächen in der Ansicht bedingt anzuzeigen:
<% if policy(@post).update? %>
<%= link_to "Edit post", edit_post_path(@post) %>
<% end %>
Wenn es eine Richtlinie ohne entsprechendes Modell/Ruby-Klasse gibt, können Sie diese durch Übergabe eines Symbols abrufen.
# 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
Beachten Sie, dass die Headless-Richtlinie weiterhin zwei Argumente akzeptieren muss. Das zweite Argument ist in diesem Fall das Symbol :dashboard
, das unten als zu authorize
Datensatz übergeben wird.
# In controllers
def show
authorize :dashboard , :show?
...
end
# In views
<% if policy(:dashboard).show? %>
<%= link_to 'Dashboard', dashboard_path %>
<% end %>
Häufig möchten Sie Datensätze anzeigen, auf die ein bestimmter Benutzer Zugriff hat. Wenn Sie Pundit verwenden, wird von Ihnen erwartet, dass Sie eine Klasse definieren, die als Richtlinienbereich bezeichnet wird. Es kann etwa so aussehen:
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 macht die folgenden Annahmen über diese Klasse:
Scope
und ist unter der Policy-Klasse verschachtelt.current_user
auf, um abzurufen, was an dieses Argument gesendet werden soll.ActiveRecord::Relation
, aber es könnte auch etwas ganz anderes sein.resolve
, die ein Ergebnis zurückgeben sollte, über das iteriert werden kann. Für ActiveRecord-Klassen wäre dies normalerweise ein ActiveRecord::Relation
.Sie möchten wahrscheinlich vom vom Generator generierten Anwendungsrichtlinienbereich erben oder Ihre eigene Basisklasse erstellen, von der Sie erben möchten:
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
Sie können diese Klasse jetzt von Ihrem Controller aus über die Methode policy_scope
verwenden:
def index
@posts = policy_scope ( Post )
end
def show
@post = policy_scope ( Post ) . find ( params [ :id ] )
end
Wie bei der Autorisierungsmethode können Sie auch die Richtlinienbereichsklasse überschreiben:
def index
# publication_class => Post
@publications = policy_scope ( publication_class , policy_scope_class : PublicationPolicy :: Scope )
end
In diesem Fall handelt es sich um eine Abkürzung für Folgendes:
def index
@publications = PublicationPolicy :: Scope . new ( current_user , Post ) . resolve
end
Sie können diese Methode in Ansichten verwenden und werden dazu ermutigt:
<% policy_scope(@user.posts).each do |post| %>
< p > <%= link_to post . title , post_path ( post ) %> </ p >
<% end %>
Wenn Sie eine Anwendung mit Pundit entwickeln, kann es leicht passieren, dass Sie vergessen, eine Aktion zu autorisieren. Die Menschen sind schließlich vergesslich. Da Pundit Sie dazu auffordert, den authorize
manuell zu jeder Controller-Aktion hinzuzufügen, ist es wirklich leicht, einen zu übersehen.
Zum Glück verfügt Pundit über eine praktische Funktion, die Sie daran erinnert, falls Sie es vergessen. Pundit verfolgt, ob Sie irgendwo in Ihrer Controller-Aktion authorize
aufgerufen haben. Pundit fügt Ihren Controllern außerdem eine Methode namens verify_authorized
hinzu. Diese Methode löst eine Ausnahme aus, wenn authorize
noch nicht aufgerufen wurde. Sie sollten diese Methode in einem after_action
Hook ausführen, um sicherzustellen, dass Sie nicht vergessen haben, die Aktion zu autorisieren. Zum Beispiel:
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
after_action :verify_authorized
end
Ebenso fügt Pundit Ihrem Controller auch verify_policy_scoped
hinzu. Dadurch wird eine Ausnahme ähnlich der von verify_authorized
ausgelöst. Es wird jedoch nachverfolgt, ob policy_scope
anstelle von authorize
verwendet wird. Dies ist vor allem für Controller-Aktionen wie index
nützlich, die Sammlungen mit einem Gültigkeitsbereich finden und keine einzelnen Instanzen autorisieren.
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
Dieser Überprüfungsmechanismus dient nur dazu, Sie bei der Entwicklung Ihrer Anwendung zu unterstützen. Vergessen Sie also nicht, authorize
aufzurufen. Es handelt sich nicht um eine Art ausfallsicheren Mechanismus oder Autorisierungsmechanismus. Sie sollten in der Lage sein, diese Filter zu entfernen, ohne dass sich dies auf die Funktionsweise Ihrer App auswirkt.
Einige Leute fanden diese Funktion verwirrend, während viele andere sie äußerst hilfreich finden. Wenn Sie zu den Leuten gehören, die es verwirrend finden, brauchen Sie es nicht zu verwenden. Pundit funktioniert ohne die Verwendung verify_authorized
und verify_policy_scoped
einwandfrei.
Wenn Sie verify_authorized
in Ihren Controllern verwenden, aber die Überprüfung bedingt umgehen müssen, können Sie skip_authorization
verwenden. Verwenden Sie zum Umgehen von verify_policy_scoped
skip_policy_scope
. Diese sind in Situationen nützlich, in denen Sie die Überprüfung nicht für die gesamte Aktion deaktivieren möchten, in einigen Fällen jedoch keine Autorisierung vornehmen möchten.
def show
record = Record . find_by ( attribute : "value" )
if record . present?
authorize record
else
skip_authorization
end
end
Manchmal möchten Sie möglicherweise explizit deklarieren, welche Richtlinie für eine bestimmte Klasse verwendet werden soll, anstatt sie von Pundit ableiten zu lassen. Dies kann folgendermaßen erfolgen:
class Post
def self . policy_class
PostablePolicy
end
end
Alternativ können Sie eine Instanzmethode deklarieren:
class Post
def policy_class
PostablePolicy
end
end
Pundit ist absichtlich eine sehr kleine Bibliothek und macht nichts, was Sie nicht auch selbst tun könnten. Hier gibt es keine geheime Soße. Es tut so wenig wie möglich und geht Ihnen dann aus dem Weg.
Mit den wenigen, aber leistungsstarken Helfern, die in Pundit verfügbar sind, haben Sie die Möglichkeit, ein gut strukturiertes, voll funktionsfähiges Autorisierungssystem aufzubauen, ohne spezielle DSLs oder komplizierte Syntax zu verwenden.
Denken Sie daran, dass alle Policy- und Scope-Klassen einfache Ruby-Klassen sind, was bedeutet, dass Sie dieselben Mechanismen verwenden können, die Sie immer verwenden, um Dinge trocken zu legen. Kapseln Sie eine Reihe von Berechtigungen in einem Modul und schließen Sie sie in mehrere Richtlinien ein. Verwenden Sie alias_method
um dafür zu sorgen, dass sich einige Berechtigungen genauso verhalten wie andere. Erben Sie von einem Basissatz an Berechtigungen. Verwenden Sie Metaprogrammierung, wenn Sie es wirklich müssen.
Verwenden Sie den mitgelieferten Generator, um Richtlinien zu generieren:
rails g pundit:policy post
In vielen Anwendungen können nur angemeldete Benutzer wirklich etwas tun. Wenn Sie ein solches System erstellen, kann es etwas umständlich sein, zu überprüfen, ob der Benutzer in einer Richtlinie nicht für jede einzelne Berechtigung nil
ist. Abgesehen von Richtlinien können Sie diese Prüfung zur Basisklasse für Bereiche hinzufügen.
Wir empfehlen Ihnen, einen Filter zu definieren, der nicht authentifizierte Benutzer auf die Anmeldeseite umleitet. Wenn Sie eine ApplicationPolicy definiert haben, ist es als sekundäre Verteidigung möglicherweise eine gute Idee, eine Ausnahme auszulösen, wenn ein nicht authentifizierter Benutzer irgendwie durchgekommen ist. Auf diese Weise können Sie eleganter scheitern.
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
Um ein Nullobjektmuster zu unterstützen, möchten Sie möglicherweise eine NilClassPolicy
implementieren. Dies kann nützlich sein, wenn Sie Ihre ApplicationPolicy erweitern möchten, um beispielsweise eine gewisse Toleranz für Assoziationen zu ermöglichen, die möglicherweise nil
sind.
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 löst einen Pundit::NotAuthorizedError
aus, den Sie in Ihrem ApplicationController
retten können. Sie können die Methode user_not_authorized
in jedem Controller anpassen.
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
Alternativ können Sie Pundit::NotAuthorizedErrors global behandeln, indem Sie Rails sie als 403-Fehler behandeln lassen und eine 403-Fehlerseite bereitstellen. Fügen Sie application.rb Folgendes hinzu:
config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden
NotAuthorizedError
s liefern Informationen darüber, welche Abfrage (z. B. :create?
), welcher Datensatz (z. B. eine Instanz von Post
) und welche Richtlinie (z. B. eine Instanz von PostPolicy
) den Fehler verursacht haben.
Eine Möglichkeit, diese query
, record
und policy
zu verwenden, besteht darin, sie mit I18n
zu verbinden, um Fehlermeldungen zu generieren. Hier erfahren Sie, wie Sie dabei vorgehen könnten.
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! '
Dies ist ein Beispiel. Pundit weiß nicht, wie Sie Ihre Fehlermeldungen implementieren.
Manchmal möchten Sie eine Richtlinie für einen Datensatz außerhalb des Controllers oder der Ansicht abrufen. Zum Beispiel, wenn Sie Berechtigungen von einer Richtlinie an eine andere delegieren.
Sie können Richtlinien und Bereiche ganz einfach wie folgt abrufen:
Pundit . policy! ( user , post )
Pundit . policy ( user , post )
Pundit . policy_scope! ( user , Post )
Pundit . policy_scope ( user , Post )
Die Bang-Methoden lösen eine Ausnahme aus, wenn die Richtlinie nicht existiert, wohingegen diejenigen ohne Bang Null zurückgeben.
Gelegentlich kann Ihr Controller möglicherweise nicht auf current_user
zugreifen oder die Methode, die von Pundit aufgerufen werden sollte, ist möglicherweise nicht current_user
. Um dieses Problem zu beheben, können Sie in Ihrem Controller eine Methode mit dem Namen pundit_user
definieren.
def pundit_user
User . find_by_other_means
end
In manchen Fällen kann es hilfreich sein, mehrere Richtlinien zu haben, die unterschiedliche Kontexte für eine Ressource bedienen. Ein Paradebeispiel hierfür ist der Fall, dass sich Benutzerrichtlinien von Administratorrichtlinien unterscheiden. Um eine Autorisierung mit einer Namespace-Richtlinie durchzuführen, übergeben Sie den Namespace in einem Array an den authorize
:
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
Wenn Sie Namespace-Richtlinien für beispielsweise Admin-Ansichten verwenden, kann es nützlich sein, policy_scope
zu überschreiben und Helfer in Ihrem AdminController
authorize
, den Namespace automatisch anzuwenden:
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 empfiehlt Ihnen dringend, Ihre Anwendung so zu modellieren, dass der einzige Kontext, den Sie für die Autorisierung benötigen, ein Benutzerobjekt und ein Domänenmodell ist, für das Sie die Autorisierung überprüfen möchten. Wenn Sie mehr Kontext benötigen, überlegen Sie, ob Sie das richtige Domänenmodell autorisieren. Vielleicht kann ein anderes Domänenmodell (oder ein Wrapper um mehrere Domänenmodelle) den benötigten Kontext bereitstellen.
Pundit erlaubt Ihnen aus genau diesem Grund nicht, zusätzliche Argumente an Richtlinien zu übergeben.
In sehr seltenen Fällen müssen Sie die Autorisierung jedoch möglicherweise auf der Grundlage von mehr Kontext als nur dem aktuell authentifizierten Benutzer durchführen. Nehmen wir beispielsweise an, dass die Autorisierung zusätzlich zum authentifizierten Benutzer von der IP-Adresse abhängt. In diesem Fall besteht eine Möglichkeit darin, eine spezielle Klasse zu erstellen, die sowohl Benutzer als auch IP einschließt und an die Richtlinie übergibt.
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
In Rails wird der Massenzuweisungsschutz im Controller verwaltet. Mit Pundit können Sie über Ihre Richtlinien steuern, welche Attribute ein Benutzer aktualisieren kann. Sie können in Ihrer Richtlinie eine Methode permitted_attributes
wie folgt einrichten:
# 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
Sie können jetzt diese Attribute aus der Richtlinie abrufen:
# 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
Dies ist jedoch etwas umständlich, weshalb Pundit eine praktische Hilfsmethode bereitstellt:
# 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
Wenn Sie basierend auf der aktuellen Aktion unterschiedliche Attribute zulassen möchten, können Sie in Ihrer Richtlinie eine Methode permitted_attributes_for_#{action}
definieren:
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def permitted_attributes_for_create
[ :title , :body ]
end
def permitted_attributes_for_edit
[ :body ]
end
end
Wenn Sie in Ihrer Richtlinie eine aktionsspezifische Methode für die aktuelle Aktion definiert haben, ruft der Helfer permitted_attributes
diese anstelle von o auf