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
帮助程序将调用它