diff --git a/CHANGELOG.md b/CHANGELOG.md index b16cddb..95362c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Added + +- Add `Pundit::Authorization#pundit_reset!` hook to reset the policy and policy scope cache. (#830) + ## 2.4.0 (2024-08-26) ## Changed diff --git a/README.md b/README.md index a1b5986..9bbe981 100644 --- a/README.md +++ b/README.md @@ -582,6 +582,25 @@ def pundit_user User.find_by_other_means end ``` +### Handling User Switching in Pundit + +When switching users in your application, it's important to reset the Pundit user context to ensure that authorization policies are applied correctly for the new user. Pundit caches the user context, so failing to reset it could result in incorrect permissions being applied. + +To handle user switching, you can use the following pattern in your controller: + +```ruby +class ApplicationController + include Pundit::Authorization + before_action :switch_user, if: :should_switch_user? + + def switch_user + current_user = User.find(params[:user_id]) + pundit_reset! # Ensure that the Pundit context is reset for the new user + end +end +``` + +Make sure to invoke `pundit_reset!` whenever changing the user. This ensures the cached authorization context is reset, preventing any incorrect permissions from being applied. ## Policy Namespacing In some cases it might be helpful to have multiple policies that serve different contexts for a diff --git a/lib/pundit/authorization.rb b/lib/pundit/authorization.rb index c0a7783..93a6240 100644 --- a/lib/pundit/authorization.rb +++ b/lib/pundit/authorization.rb @@ -217,5 +217,27 @@ def pundit_params_for(record) end # @!endgroup + + # @!group Customize Pundit user + + # Clears the cached Pundit authorization data. + # + # This method should be called when the pundit_user is changed, + # such as during user switching, to ensure that stale authorization + # data is not used. Pundit caches authorization policies and scopes + # for the pundit_user, so calling this method will reset those + # caches and ensure that the next authorization checks are performed + # with the correct context for the new pundit_user. + # + # @return [void] + def pundit_reset! + @pundit = nil + @_pundit_policies = nil + @_pundit_policy_scopes = nil + @_pundit_policy_authorized = nil + @_pundit_policy_scoped = nil + end + + # @!endgroup end end diff --git a/spec/authorization_spec.rb b/spec/authorization_spec.rb index 8bfa3fc..24ec231 100644 --- a/spec/authorization_spec.rb +++ b/spec/authorization_spec.rb @@ -271,4 +271,49 @@ def to_params(*args, **kwargs, &block) expect(Controller.new(user, action, params).permitted_attributes(post, :revise).to_h).to eq("body" => "blah") end end + + describe "#pundit_reset!" do + it "allows authorize to react to a user change" do + expect(controller.authorize(post)).to be_truthy + controller.current_user = double + controller.pundit_reset! + expect { controller.authorize(post) }.to raise_error(Pundit::NotAuthorizedError) + end + + it "allows policy scope to react to a user change" do + expect(controller.policy_scope(Post)).to eq :published + expect { controller.verify_policy_scoped }.not_to raise_error + controller.current_user = double + controller.pundit_reset! + expect { controller.verify_policy_scoped }.to raise_error(Pundit::PolicyScopingNotPerformedError) + end + + it "clears the pundit context user" do + expect(controller.pundit.user).to be(user) + + new_user = double + controller.current_user = new_user + expect { controller.pundit_reset! }.to change { controller.pundit.user }.from(user).to(new_user) + end + + it "clears pundit_policy_authorized? flag" do + expect(controller.pundit_policy_authorized?).to be false + + controller.skip_authorization + expect(controller.pundit_policy_authorized?).to be true + + controller.pundit_reset! + expect(controller.pundit_policy_authorized?).to be false + end + + it "clears pundit_policy_scoped? flag" do + expect(controller.pundit_policy_scoped?).to be false + + controller.skip_policy_scope + expect(controller.pundit_policy_scoped?).to be true + + controller.pundit_reset! + expect(controller.pundit_policy_scoped?).to be false + end + end end diff --git a/spec/support/lib/controller.rb b/spec/support/lib/controller.rb index 4077de2..8715ecf 100644 --- a/spec/support/lib/controller.rb +++ b/spec/support/lib/controller.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true class Controller - attr_reader :current_user, :action_name, :params + attr_accessor :current_user + attr_reader :action_name, :params class View def initialize(controller)