Pundit 提供了一組幫助程序,指導您利用常規 Ruby 類別和物件導向的設計模式來建立簡單、強大且可擴展的授權系統。
贊助商:瓦爾維特
請注意,GitHub 上的 README 與GitHub 上的最新程式碼是準確的。您很可能使用 Pundit 的發布版本,因此請參閱 Pundit 最新發布版本的文件。
bundle add pundit
在您的應用程式控制器中包含Pundit::Authorization
:
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
end
或者,您可以運行生成器,它將設定一個應用程式策略,其中包含一些有用的預設值:
rails g pundit:install
產生應用程式原則後,重新啟動 Rails 伺服器,以便 Rails 可以選取新的app/policies/
目錄中的任何類別。
專家關注的是政策類別的概念。我們建議您將這些類別放在app/policies
中。這是一個範例,如果使用者是管理員,或貼文未發布,則允許更新貼文:
class PostPolicy
attr_reader :user , :post
def initialize ( user , post )
@user = user
@post = post
end
def update?
user . admin? || ! post . published?
end
end
正如您所看到的,這是一個普通的 Ruby 類別。 Pundit 對此類做出以下假設:
current_user
方法來擷取要傳送至此參數的內容update?
。通常,這將對應到特定控制器操作的名稱。確實如此。
通常您需要繼承由生成器建立的應用程式策略,或設定您自己的基底類別來繼承:
class PostPolicy < ApplicationPolicy
def update?
user . admin? or not record . published?
end
end
在產生的ApplicationPolicy
中,模型物件稱為record
。
假設您有一個Post
類別的實例,Pundit 現在允許您在控制器中執行此操作:
def update
@post = Post . find ( params [ :id ] )
authorize @post
if @post . update ( post_params )
redirect_to @post
else
render :edit
end
end
Authorize 方法自動推斷Post
將有一個匹配的PostPolicy
類,並實例化該類,並提交當前使用者和給定的記錄。然後它從操作名稱推斷它應該調用update?
在這個政策實例上。在這種情況下,您可以想像authorize
會執行以下操作:
unless PostPolicy . new ( current_user , @post ) . update?
raise Pundit :: NotAuthorizedError , "not allowed to PostPolicy#update? this Post"
end
如果您要檢查的權限名稱與操作名稱不匹配,您可以傳遞第二個參數進行authorize
。例如:
def publish
@post = Post . find ( params [ :id ] )
authorize @post , :update?
@post . publish!
redirect_to @post
end
如有必要,您可以傳遞參數來覆寫策略類別。例如:
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
如果您沒有authorize
的第一個參數的實例,那麼您可以傳遞該類別。例如:
政策:
class PostPolicy < ApplicationPolicy
def admin_list?
user . admin?
end
end
控制器:
def admin_list
authorize Post # we don't have a particular post to authorize
# Rest of controller action
end
authorize
返回傳遞給它的實例,因此您可以像這樣連結它:
控制器:
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
您可以透過視圖和控制器中的policy
方法輕鬆取得策略的實例。這對於有條件地在視圖中顯示連結或按鈕特別有用:
<% if policy(@post).update? %>
<%= link_to "Edit post", edit_post_path(@post) %>
<% end %>
假設有一個策略沒有相應的模型/ruby 類,您可以透過傳遞符號來檢索它。
# 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
請注意,無頭策略仍然需要接受兩個參數。在本例中,第二個參數將是符號:dashboard
,它是作為下面要authorize
的記錄傳遞的內容。
# In controllers
def show
authorize :dashboard , :show?
...
end
# In views
<% if policy(:dashboard).show? %>
<%= link_to 'Dashboard', dashboard_path %>
<% end %>
通常,您會希望擁有某種特定使用者有權存取的視圖清單記錄。使用 Pundit 時,您需要定義一個稱為策略範圍的類別。它可能看起來像這樣:
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 對此類做出以下假設:
Scope
,並且嵌套在策略類別下。current_user
方法來檢索要傳送至此參數的內容。ActiveRecord::Relation
,但也可能完全是其他東西。resolve
,該方法應該傳回某種可以迭代的結果。對於 ActiveRecord 類,這通常是ActiveRecord::Relation
。您可能希望從生成器產生的應用程式策略範圍繼承,或建立您自己的基類來繼承:
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
您現在可以透過policy_scope
方法從控制器使用此類:
def index
@posts = policy_scope ( Post )
end
def show
@post = policy_scope ( Post ) . find ( params [ :id ] )
end
與授權方法一樣,您也可以覆寫策略範圍類別:
def index
# publication_class => Post
@publications = policy_scope ( publication_class , policy_scope_class : PublicationPolicy :: Scope )
end
在這種情況下,這是執行以下操作的捷徑:
def index
@publications = PublicationPolicy :: Scope . new ( current_user , Post ) . resolve
end
我們鼓勵您在視圖中使用此方法:
<% policy_scope(@user.posts).each do |post| %>
< p > <%= link_to post . title , post_path ( post ) %> </ p >
<% end %>
當您使用 Pundit 開發應用程式時,很容易忘記授權某些操作。人終究是健忘的。由於 Pundit 鼓勵您手動將authorize
呼叫新增至每個控制器操作中,因此很容易錯過一個。
值得慶幸的是,Pundit 有一個方便的功能,可以在您忘記時提醒您。 Pundit 追蹤您是否在控制器操作中的任何位置呼叫了authorize
。 Pundit 也為您的控制器新增了一個名為verify_authorized
的方法。如果尚未呼叫authorize
此方法將引發異常。您應該在after_action
掛鉤中執行此方法,以確保您沒有忘記授權該操作。例如:
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
after_action :verify_authorized
end
同樣,Pundit 還將verify_policy_scoped
添加到您的控制器中。這將引發類似於verify_authorized
異常。但是,它會追蹤是否使用了policy_scope
而不是authorize
。這對於像index
這樣的控制器操作非常有用,它可以尋找具有範圍的集合並且不授權單一實例。
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
此驗證機制的存在只是為了在開發應用程式時為您提供幫助,因此您不要忘記呼叫authorize
。它不是某種故障安全機製或授權機制。您應該能夠刪除這些過濾器,而不會以任何方式影響您的應用程式的工作方式。
有些人發現這個功能令人困惑,而其他許多人則發現它非常有用。如果您屬於覺得它令人困惑的人類別,那麼您就不需要使用它。 Pundit 無需使用verify_authorized
和verify_policy_scoped
即可正常運作。
如果您在控制器中使用verify_authorized
但需要有條件地繞過驗證,則可以使用skip_authorization
。要繞過verify_policy_scoped
,請使用skip_policy_scope
。當您不想停用整個操作的驗證,但在某些情況下您不想授權時,這些功能非常有用。
def show
record = Record . find_by ( attribute : "value" )
if record . present?
authorize record
else
skip_authorization
end
end
有時您可能想要明確聲明對給定類別使用哪個策略,而不是讓 Pundit 推斷它。這可以像這樣完成:
class Post
def self . policy_class
PostablePolicy
end
end
或者,您可以聲明一個實例方法:
class Post
def policy_class
PostablePolicy
end
end
Pundit 是一個特意設計的非常小的函式庫,它不會做任何你自己做不到的事情。這裡沒有秘密武器。它會盡量少做事,然後就不妨礙你了。
借助 Pundit 中提供的少數但功能強大的幫助程序,您可以構建結構良好、功能齊全的授權系統,而無需使用任何特殊的 DSL 或時髦的語法。
請記住,所有策略和作用域類別都是普通的 Ruby 類,這意味著您可以使用通常用於 DRY 內容的相同機制。將一組權限封裝到一個模組中,並將它們包含在多個策略中。使用alias_method
使某些權限的行為與其他權限相同。從一組基本權限繼承。如果確實需要,請使用元編程。
使用提供的生成器產生策略:
rails g pundit:policy post
在許多應用程式中,只有登入的使用者才真正能夠執行任何操作。如果您正在建立這樣的系統,那麼檢查策略中的使用者的每個權限是否都不是nil
可能會很麻煩。除了策略之外,您還可以將此檢查新增到範圍的基類中。
我們建議您定義一個過濾器,將未經身份驗證的使用者重新導向到登入頁面。作為輔助防禦,如果您定義了 ApplicationPolicy,那麼在未經身份驗證的使用者以某種方式通過時引發異常可能是個好主意。這樣你就可以更優雅地失敗。
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
為了支援空物件模式,您可能會發現需要實作NilClassPolicy
。當您想要擴展 ApplicationPolicy 以允許一定程度的容忍(例如,可能為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 會引發Pundit::NotAuthorizedError
您可以在ApplicationController
中rescue_from 。您可以在每個控制器中自訂user_not_authorized
方法。
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
或者,您可以透過讓 Rails 將 Pundit::NotAuthorizedError 作為 403 錯誤處理並提供 403 錯誤頁面來全域處理它們。將以下內容新增至 application.rb:
config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden
NotAuthorizedError
提供有關什麼查詢(例如:create?
)、什麼記錄(例如Post
實例)以及什麼策略(例如PostPolicy
實例)導致引發錯誤的資訊。
使用這些query
、 record
和policy
屬性的一種方法是將它們與I18n
連接以產生錯誤訊息。以下是您可以採取的方法。
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! '
這是一個例子。 Pundit 不知道您如何實現錯誤訊息傳遞。
有時您想要擷取控制器或檢視以外的記錄的策略。例如,當您將權限從一項策略委託給另一項策略時。
您可以輕鬆檢索策略和範圍,如下所示:
Pundit . policy! ( user , post )
Pundit . policy ( user , post )
Pundit . policy_scope! ( user , Post )
Pundit . policy_scope ( user , Post )
如果策略不存在,bang 方法將引發異常,而沒有 bang 的方法將傳回 nil。
有時,您的控制器可能無法存取current_user
,或者 Pundit 應該呼叫的方法可能不是current_user
。為了解決這個問題,您可以在控制器中定義一個名為pundit_user
方法。
def pundit_user
User . find_by_other_means
end
在某些情況下,擁有多個為資源的不同上下文提供服務的策略可能會有所幫助。一個典型的例子是使用者策略與管理策略不同的情況。若要使用命名空間策略進行授權,請將命名空間傳遞到陣列中的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
如果您將命名空間原則用於管理視圖之類的內容,則覆寫policy_scope
並authorize
AdminController
中的幫助程式自動套用命名空間可能會很有用:
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 強烈鼓勵您以這樣的方式對應用程式進行建模:授權所需的唯一上下文是使用者物件和要檢查授權的網域模型。如果您發現自己需要更多上下文,請考慮您是否正在授權正確的域模型,也許另一個域模型(或多個域模型的包裝器)可以提供您需要的上下文。
正是出於這個原因,Pundit 不允許您向策略傳遞額外的參數。
但是,在極少數情況下,您可能需要根據更多上下文進行授權,而不僅僅是當前經過身份驗證的使用者。例如,假設除了經過身份驗證的使用者之外,授權還依賴 IP 位址。在這種情況下,一種選擇是建立一個特殊的類別來包裝使用者和 IP 並將其傳遞給策略。
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
在 Rails 中,批量分配保護是在控制器中處理的。借助 Pundit,您可以透過您的策略控制使用者有權更新哪些屬性。您可以在策略中設定permitted_attributes
方法,如下所示:
# 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
現在您可以從策略中檢索這些屬性:
# 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
不過,這有點麻煩,所以 Pundit 提供了一個方便的輔助方法:
# 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
如果您想要根據目前操作允許不同的屬性,您可以在策略中定義permitted_attributes_for_#{action}
方法:
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def permitted_attributes_for_create
[ :title , :body ]
end
def permitted_attributes_for_edit
[ :body ]
end
end
如果您在策略上為目前操作定義了特定於操作的方法,則permitted_attributes
幫助程式將呼叫它