gem "pundit"
Include Pundit in your application controller:
class ApplicationController < ActionController::Base include Pundit protect_from_forgery end
Optionally, you can run the generator, which will set up an application policy with some useful defaults for you:
rails g pundit:install
After generating your application policy, restart the Rails server so that Rails can pick up any classes in the new app/policies/
directory.
Pundit is focused around the notion of policy classes. We suggest that you put these classes in app/policies
. This is a simple example that allows updating a post if the user is an admin, or if the post is unpublished:
pundit的重点是围绕policy的概念。我们建议你把这些类放到 app/policies。这是一个简单的例子,如果用户是管理员,它允许更新一个post,有可能该post是未发表的:
class PostPolicy attr_reader :user, :post #定义reader变量 def initialize(user, post) #初始化实例变量 @user = user @post = post end def update? #定义更新方法 user.admin? or not post.published? #user是否有admin权限,或者post是否已经发表 end end
As you can see, this is just a plain Ruby class. Pundit makes the following assumptions about this class:
current_user
method to retrieve what to send into this argumentupdate?
. Usually, this will map to the name of a particular controller action.That's it really.
Usually you'll want to inherit from the application policy created by the generator, or set up your own base class to inherit from:
正如你所看到的,这只是一个普通的Ruby类。pundit关于这个类有以下假设:
这个类有相同的名称为某种model类,只有后缀词“policy”。
第一个参数是一个用户。在你的控制器,pundit将调用CURRENT_USER方法来检索把什么传进这个参数
第二个参数是某种模型对象,要检查其授权。这并不需要是一个ActiveRecord甚至是加载ActiveModel对象,它可以真正做到任何事情。
这个类实现某种查询方法,在这种情况下更新?通常,这将映射到一个特定的控制器动作的名称。
这是真的。
通常你会想从generator产生的应用程序policy继承,或建立自己的基类继承自:
class PostPolicy < ApplicationPolicy def update? user.admin? or not record.published? end end
In the generated ApplicationPolicy
, the model object is called record
.
Supposing that you have an instance of class Post
, Pundit now lets you do this in your controller:
在生成ApplicationPolicy,model对象被称为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
The authorize method automatically infers that Post
will have a matching PostPolicy
class, and instantiates this class, handing in the current user and the given record. It then infers from the action name, that it should call update?
on this instance of the policy. In this case, you can imagine that authorize
would have done something like this:
该authorize方法将自动推断Post
有一个匹配的PostPolicy类,实例化这个类,交给在当前用户和给定的记录。然后,从动作的名称推断,在policy的这个实例里它应该调用update? 。在这种情况下,你可以想像authorize
会做这样的事情:
raise "not authorized" unless PostPolicy.new(current_user, @post).update?
You can pass a second argument to authorize
if the name of the permission you want to check doesn't match the action name. For example:
你可以通过第二个参数授权,检查的权限名是否不匹配动作名。例如:
def publish @post = Post.find(params[:id]) authorize @post, :update? @post.publish! redirect_to @post end
You can easily get a hold of an instance of the policy through the policy
method in both the view and controller. This is especially useful for conditionally showing links or buttons in the view:
<% if policy(@post).update? %> <%= link_to "Edit post", edit_post_path(@post) %> <% end %>
Headless policies
Given there is a policy without a corresponding model / ruby class, you can retrieve it by passing a symbol.
鉴于有policy没有相应的 model/ Ruby类,你可以通过传递一个符号进行检索。
# app/policies/dashboard_policy.rb class DashboardPolicy < Struct.new(:user, :dashboard) # ... end
# In controllers authorize :dashboard, :show?
# In views <% if policy(:dashboard).show? %> <%= link_to 'Dashboard', dashboard_path %> <% end %>
Ensuring policies are used
Pundit adds a method called verify_authorized
to your controllers. This method will raise an exception if authorize
has not yet been called. You should run this method in an after_action
to ensure that you haven't forgotten to authorize the action. For example:
pubdit添加一个名为verify_authorized到你的控制器的方法。此方法将引发异常,如果授权还没有被调用。你应该在after_action运行此方法,以确保你没有忘记授权的行动。例如:
class ApplicationController < ActionController::Base after_action :verify_authorized, :except => :index end
Likewise, Pundit also adds verify_policy_scoped to your controller. This will raise an exception in the vein of verify_authorized. However, it tracks if policy_scope is used instead of authorize. This is mostly useful for controller actions like index which find collections with a scope and don't authorize individual instances.
同样,pundit还增加了verify_policy_scoped到控制器。这将抛出一个异常在vein的verify_authorized。然而,它跟踪policy_scope是否是用来代替认证。对控制器的动作来说,这是很有用的,就像索引一样,来用一个scope查找组合和不授权单个实例。
class ApplicationController < ActionController::Base after_action :verify_policy_scoped, :only => :index end
If you're using verify_authorized
in your controllers but need to conditionally bypass verification, you can use skip_authorization
. For bypassing verify_policy_scoped
, use skip_policy_scope
. These are useful in circumstances where you don't want to disable verification for the entire action, but have some cases where you intend to not authorize.
如果您使用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
If you need to perform some more sophisticated logic or you want to raise a custom exception you can use the two lower level methods pundit_policy_authorized?
and pundit_policy_scoped?
which return true
or false
depending on whether authorize
or policy_scope
have been called, respectively.
如果您需要执行一些更加复杂的逻辑,或者你想提出一个自定义异常,您可以使用两个较低水平的方法pundit_policy_scoped?和pundit_policy_authorized? ,它返回true或false取决于是否授权或policy_scope已分别调用。
Often, you will want to have some kind of view listing records which a particular user has access to. When using Pundit, you are expected to define a class called a policy scope. It can look something like this:
class PostPolicy < ApplicationPolicy class Scope attr_reader :user, :scope def initialize(user, scope) @user = user @scope = scope end def resolve if user.admin? scope.all else scope.where(:published => true) end end end def update? user.admin? or not post.published? end end
Pundit makes the following assumptions about this class:
Scope
and is nested under the policy class.current_user
method to retrieve what to send into this argument.ActiveRecord::Relation
, but it could be something else entirely.resolve
, which should return some kind of result which can be iterated over. For ActiveRecord classes, this would usually be an ActiveRecord::Relation
.You'll probably want to inherit from the application policy scope generated by the generator, or create your own base class to inherit from:
class PostPolicy < ApplicationPolicy class Scope < Scope def resolve if user.admin? scope.all else scope.where(:published => true) end end end def update? user.admin? or not post.published? end end
You can now use this class from your controller via the policy_scope
method:
你可以使用你的控制器通过policy_scope方法:
def index @posts = policy_scope(Post) end
Just as with your policy, this will automatically infer that you want to use the PostPolicy::Scope
class, it will instantiate this class and call resolve
on the instance. In this case it is a shortcut for doing:
正如你的policy,这将自动推断出你想要使用的PostPolicy::Scope
类,它将实例化这个类,并调用resolve的实例。在这种情况下,它是做一个快捷方式:
def index @posts = PostPolicy::Scope.new(current_user, Post).resolve end
You can, and are encouraged to, use this method in views:
我们鼓励使用这个方法在views中:
<% policy_scope(@user.posts).each do |post| %> <p><%= link_to post.title, post_path(post) %></p> <% end %>
Manually specifying policy classes
Sometimes you might want to explicitly declare which policy to use for a given class, instead of letting Pundit infer it. This can be done like so:
有时你可能需要明确声明,哪一个policy让给定的类使用,代替继承它的给定类。这是可以做到像这样:
class Post def self.policy_class PostablePolicy end end
As you can see, Pundit doesn't do anything you couldn't have easily done yourself. It's a very small library, it just provides a few neat helpers. Together these give you the power of building a well structured, fully working authorization system without using any special DSLs or funky syntax or anything.
Remember that all of the policy and scope classes are just plain Ruby classes, which means you can use the same mechanisms you always use to DRY things up. Encapsulate a set of permissions into a module and include them in multiple policies. Use alias_method
to make some permissions behave the same as others. Inherit from a base set of permissions. Use metaprogramming if you really have to.
正如你所看到的,pundit没有做任何事情阻碍你做自己。这是一个非常小的库,它只是提供了一些简洁的帮助。连同这些帮你建立一个结构良好,充分的工作授权系统,而无需使用任何特殊的DSL或时髦的语法或任何。
请记住,所有的policy和scope类的只是普通的Ruby类,这意味着你可以使用相同的机制,你总是用DRY就好了。封装一组权限到一个模块并包含在多个policy。和其他的一样使用alias_method做出一些权限。继承自一些基本的权限集合。如果你真的不得不这样做使用元编程。
Use the supplied generator to generate policies:
使用generator提供的来生成policies:
rails g pundit:policy post
Closed systems
In many applications, only logged in users are really able to do anything. If you're building such a system, it can be kind of cumbersome to check that the user in a policy isn't nil
for every single permission.
We suggest that you define a filter that redirects unauthenticated users to the login page. As a secondary defence, if you've defined an ApplicationPolicy, it might be a good idea to raise an exception if somehow an unauthenticated user got through. This way you can fail more gracefully.
在许多应用中,只有登录用户真的能够做任何事情。如果你正在构建这样一个系统,它可以是那种繁琐的检查对没个权限来说,用户在policy中不是nil。
我们建议您定义一个未经验证的用户重定向到登录页面的过滤器。作为一个间接的防御,如果你定义一个ApplicationPolicy,不知怎么回事一个未验证的用户通过了,这是一个报错的好方式。这样,你可以将异常处理的更好。
class ApplicationPolicy def initialize(user, record) raise Pundit::NotAuthorizedError, "must be logged in" unless user @user = user @record = record end end
Rescuing a denied Authorization in Rails
Pundit raises a Pundit::NotAuthorizedError
you can rescue_from in your ApplicationController
. You can customize the user_not_authorized
method in every controller.
puntid提供Pundit::NotAuthorizedError
,你能够使用rescue_from在你的applicaitonController.你能在每个controller中自定义user_not_authorized方法:
class ApplicationController < ActionController::Base protect_from_forgery include Pundit rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized private def user_not_authorized flash[:alert] = "You are not authorized to perform this action." redirect_to(request.referrer || root_path) end end
Creating custom error messages
NotAuthorizedError
s provide information on what query (e.g. :create?
), what record (e.g. an instance of Post
), and what policy (e.g. an instance of PostPolicy
) caused the error to be raised.
One way to use these query
, record
, and policy
properties is to connect them with I18n
to generate error messages. Here's how you might go about doing that.
NotAuthorizedError
s提供了有关于什么查询(如:create),哪一个record(如:Post的一个实例),哪一个policy(如:PostPolicy的一个实例)的报错信息
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_to(request.referrer || 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!'
Of course, this is just an example. Pundit is agnostic as to how you implement your error messaging.
当然,这仅仅是一个例子。pundit并不知道你如何实现你的错误消息。
Manually retrieving policies and scopes
Sometimes you want to retrieve a policy for a record outside the controller or view. For example when you delegate permissions from one policy to another.
You can easily retrieve policies and scopes like this:
有时你想检索一个控制器或者view之外的一个policy。例如当你委派一个policy的权限到另一个:
Pundit.policy!(user, post) Pundit.policy(user, post) Pundit.policy_scope!(user, Post) Pundit.policy_scope(user, Post)
The bang methods will raise an exception if the policy does not exist, whereas those without the bang will return nil.
当policy不存在时,带感叹号的感发将会报错,不带感叹号的就将返回nil.
In some cases your controller might not have access to current_user
, or your current_user
is not the method that should be invoked by Pundit. Simply define a method in your controller called pundit_user
.
在某些情况下,控制器可能不能访问CURRENT_USER,或者使用current_user不应该由pundit调用。简单地定义你的控制器名为pundit_user。
def pundit_user User.find_by_other_means end
Additional context
Pundit strongly encourages you to model your application in such a way that the only context you need for authorization is a user object and a domain model that you want to check authorization for. If you find yourself needing more context than that, consider whether you are authorizing the right domain model, maybe another domain model (or a wrapper around multiple domain models) can provide the context you need.
Pundit does not allow you to pass additional arguments to policies for precisely this reason.
However, in very rare cases, you might need to authorize based on more context than just the currently authenticated user. Suppose for example that authorization is dependent on IP address in addition to the authenticated user. In that case, one option is to create a special class which wraps up both user and IP and passes it to the policy.
class UserContext attr_reader :user, :ip def initialize(user, ip) @user = user @ip = ip end end class ApplicationController include Pundit def pundit_user UserContext.new(current_user, request.ip) end end
Strong parameters
In Rails 4 (or Rails 3.2 with the strong_parameters gem), mass-assignment protection is handled in the controller. With Pundit you can control which attributes a user has access to update via your policies. You can set up a permitted_attributes
method in your policy like this:
在Rails4(或者Rails3.2的strong_parameters gem中),mass-assignment保护控制器处理。使用pundit您可以控制哪些属性用户有权访问通过您的policy进行更新。您可以设置一个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
You can now retrieve these attributes from the policy:
现在你可以在policy中检索这些属性
# app/controllers/posts_controller.rb class PostsController < ApplicationController def update @post = Post.find(params[:id]) if @post.update_attributes(post_params) redirect_to @post else render :edit end end private def post_params params.require(:post).permit(policy(@post).permitted_attributes) end end
However, this is a bit cumbersome, so Pundit provides a convenient helper method:
然而,这有点复杂,所以pundit提供一个简便的helper方法:
# app/controllers/posts_controller.rb class PostsController < ApplicationController def update @post = Post.find(params[:id]) if @post.update_attributes(permitted_attributes(@post)) redirect_to @post else render :edit end end end
RSpec
Pundit includes a mini-DSL for writing expressive tests for your policies in RSpec. Require pundit/rspec
in your spec_helper.rb
:
在Rspec中pundit为你的policies提供一个迷你DSL测试写法,需要在pundit/rspec下编写
spec_helper.rb
:
require "pundit/rspec"
Then put your policy specs in spec/policies
, and make them look somewhat like this:
之后你可以在spec/policies
下写policy specs,写法类似这样:
describe PostPolicy do subject { described_class } permissions :update? do it "denies access if post is published" do expect(subject).not_to permit(User.new(:admin => false), Post.new(:published => true)) end it "grants access if post is published and user is an admin" do expect(subject).to permit(User.new(:admin => true), Post.new(:published => true)) end it "grants access if post is unpublished" do expect(subject).to permit(User.new(:admin => false), Post.new(:published => false)) end end end
An alternative approach to Pundit policy specs is scoping them to a user context as outlined in this excellent post.
另一种写policy specs方法在这篇优秀的文章里