From 5f5a3503ca35c595b9d1eec911f3b80ab25e13ff Mon Sep 17 00:00:00 2001 From: Eric Guo Date: Fri, 11 Feb 2022 20:47:16 +0800 Subject: [PATCH] Copy #443, allow define additional methods to the scope. --- README.md | 39 +++++++++++++++++++++++++++++++++++++ lib/pundit.rb | 14 +++++++------ lib/pundit/authorization.rb | 12 ++++++++---- spec/authorization_spec.rb | 4 ++++ spec/pundit_spec.rb | 8 ++++++++ spec/spec_helper.rb | 8 ++++++++ 6 files changed, 75 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 790962fa..7a07e2a9 100644 --- a/README.md +++ b/README.md @@ -332,6 +332,45 @@ You can, and are encouraged to, use this method in views: <% end %> ``` +It is also possible to define additional methods the scope that you can use in +different situations. We'll add an `unpublished` scope to the +`PostPolicy::Scope`: + +``` ruby +class PostPolicy < ApplicationPolicy + class Scope < Scope + def resolve + if user.admin? + scope.all + else + scope.where(published: true) + end + end + + def unpublished + if user.admin? + scope.all + else + scope.where(published: false) + end + end + end + + def update? + user.admin? or not post.published? + end +end +``` + +To use the `unpublished` scope, simply pass the name of the method as the 2nd +argument to `policy_scope`: + +``` ruby +def index + @posts = policy_scope(Post, :unpublished) +end +``` + ## Ensuring policies and scopes are used When you are developing an application with Pundit it can be easy to forget to diff --git a/lib/pundit.rb b/lib/pundit.rb index 5eab334f..b0efb69d 100644 --- a/lib/pundit.rb +++ b/lib/pundit.rb @@ -93,9 +93,10 @@ def authorize(user, possibly_namespaced_record, query, policy_class: nil, cache: # @see https://github.com/varvet/pundit#scopes # @param user [Object] the user that initiated the action # @param scope [Object] the object we're retrieving the policy scope for + # @param method [Symbol] the method to call on the scope. Defaults to :resolve. # @raise [InvalidConstructorError] if the policy constructor called incorrectly # @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope - def policy_scope(user, scope) + def policy_scope(user, scope, method = :resolve) policy_scope_class = PolicyFinder.new(scope).scope return unless policy_scope_class @@ -105,7 +106,7 @@ def policy_scope(user, scope) raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called" end - policy_scope.resolve + policy_scope.public_send(method) end # Retrieves the policy scope for the given record. @@ -113,10 +114,11 @@ def policy_scope(user, scope) # @see https://github.com/varvet/pundit#scopes # @param user [Object] the user that initiated the action # @param scope [Object] the object we're retrieving the policy scope for + # @param method [Symbol] the method to call on the scope. Defaults to :resolve. # @raise [NotDefinedError] if the policy scope cannot be found # @raise [InvalidConstructorError] if the policy constructor called incorrectly # @return [Scope{#resolve}] instance of scope class which can resolve to a scope - def policy_scope!(user, scope) + def policy_scope!(user, scope, method = :resolve) policy_scope_class = PolicyFinder.new(scope).scope! return unless policy_scope_class @@ -126,7 +128,7 @@ def policy_scope!(user, scope) raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called" end - policy_scope.resolve + policy_scope.public_send(method) end # Retrieves the policy for the given record. @@ -167,8 +169,8 @@ def pundit_model(record) # @api private module Helper - def policy_scope(scope) - pundit_policy_scope(scope) + def policy_scope(scope, method = :resolve) + pundit_policy_scope(scope, method) end end end diff --git a/lib/pundit/authorization.rb b/lib/pundit/authorization.rb index 1231f2a7..cc447ca3 100644 --- a/lib/pundit/authorization.rb +++ b/lib/pundit/authorization.rb @@ -87,11 +87,12 @@ def skip_policy_scope # # @see https://github.com/varvet/pundit#scopes # @param scope [Object] the object we're retrieving the policy scope for + # @param method [Symbol] the method to call on the scope. Defaults to :resolve. # @param policy_scope_class [Class] the policy scope class we want to force use of # @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope - def policy_scope(scope, policy_scope_class: nil) + def policy_scope(scope, method = :resolve, policy_scope_class: nil) @_pundit_policy_scoped = true - policy_scope_class ? policy_scope_class.new(pundit_user, scope).resolve : pundit_policy_scope(scope) + policy_scope_class ? policy_scope_class.new(pundit_user, scope).public_send(method) : pundit_policy_scope(scope, method) end # Retrieves the policy for the given record. @@ -161,8 +162,11 @@ def pundit_user private - def pundit_policy_scope(scope) - policy_scopes[scope] ||= Pundit.policy_scope!(pundit_user, scope) + def pundit_policy_scope(scope, method = :resolve) + method = method.to_sym + key = method == :resolve ? scope : [scope, method] + + policy_scopes[key] ||= Pundit.policy_scope!(pundit_user, scope, method) end end end diff --git a/spec/authorization_spec.rb b/spec/authorization_spec.rb index 2995bfdb..add736c5 100644 --- a/spec/authorization_spec.rb +++ b/spec/authorization_spec.rb @@ -145,6 +145,10 @@ expect(policy.post).to eq post end + it "returns an instantiated policy scope when a scope method is provided" do + expect(controller.policy_scope(Post, :unpublished)).to eq :unpublished + end + it "throws an exception if the given policy can't be found" do expect { controller.policy(article) }.to raise_error(Pundit::NotDefinedError) end diff --git a/spec/pundit_spec.rb b/spec/pundit_spec.rb index 6911ba63..52ebbb63 100644 --- a/spec/pundit_spec.rb +++ b/spec/pundit_spec.rb @@ -93,6 +93,10 @@ expect(Pundit.policy_scope(user, Post)).to eq :published end + it "returns an instantiated policy scope given the method to call on the scope" do + expect(Pundit.policy_scope(user, Post, :unpublished)).to eq :unpublished + end + it "returns an instantiated policy scope given an active model class" do expect(Pundit.policy_scope(user, Comment)).to eq CommentScope.new(Comment) end @@ -139,6 +143,10 @@ expect(Pundit.policy_scope!(user, Post)).to eq :published end + it "returns an instantiated policy scope given the method to call on the scope" do + expect(Pundit.policy_scope!(user, Post, :unpublished)).to eq :unpublished + end + it "returns an instantiated policy scope given an active model class" do expect(Pundit.policy_scope!(user, Comment)).to eq CommentScope.new(Comment) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 26e41b53..34383cf6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -23,6 +23,10 @@ class Scope < Struct.new(:user, :scope) def resolve scope.published end + + def unpublished + scope.unpublished + end end def update? @@ -55,6 +59,10 @@ def self.published :published end + def self.unpublished + :unpublished + end + def self.read :read end