Skip to content

Commit

Permalink
Merge pull request #830 from furkanural/add-clear-pundit-context-hook
Browse files Browse the repository at this point in the history
add hook for clear pundit context
  • Loading branch information
Burgestrand authored Nov 22, 2024
2 parents 82f635c + ddaa1e6 commit 54cc64e
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 2 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

## Added

- Add `Pundit::Authorization#pundit_reset!` hook to reset the policy and policy scope cache. (#830)

## Changed

- Deprecated `Pundit::SUFFIX`, moved it to `Pundit::PolicyFinder::SUFFIX` (#835)
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

def switch_user_to(user)
terminate_session if authenticated?
start_new_session_for user
pundit_reset!
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
Expand Down
21 changes: 21 additions & 0 deletions lib/pundit/authorization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,33 @@ def pundit
# Hook method which allows customizing which user is passed to policies and
# scopes initialized by {#authorize}, {#policy} and {#policy_scope}.
#
# @note Make sure to call `pundit_reset!` if this changes during a request.
# @see https://github.com/varvet/pundit#customize-pundit-user
# @see #pundit
# @see #pundit_reset!
# @return [Object] the user object to be used with pundit
def pundit_user
current_user
end

# 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

# @!group Policies

# Retrieves the policy for the given record, initializing it with the record
Expand Down
58 changes: 57 additions & 1 deletion spec/authorization_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def to_params(*args, **kwargs, &block)
end

let(:controller) { Controller.new(user, "update", to_params({})) }
let(:user) { double }
let(:user) { double("user") }
let(:post) { Post.new(user) }
let(:comment) { Comment.new }
let(:article) { Article.new }
Expand Down Expand Up @@ -272,4 +272,60 @@ 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 to react to a user change" do
expect(controller.policy(DummyCurrentUser).user).to be user

new_user = double("new user")
controller.current_user = new_user
controller.pundit_reset!
expect(controller.policy(DummyCurrentUser).user).to be new_user
end

it "allows policy scope to react to a user change" do
expect(controller.policy_scope(DummyCurrentUser)).to be user

new_user = double("new user")
controller.current_user = new_user
controller.pundit_reset!
expect(controller.policy_scope(DummyCurrentUser)).to be new_user
end

it "resets the pundit context" 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
3 changes: 2 additions & 1 deletion spec/support/lib/controller.rb
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
7 changes: 7 additions & 0 deletions spec/support/models/dummy_current_user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class DummyCurrentUser
def update?
user
end
end
9 changes: 9 additions & 0 deletions spec/support/policies/dummy_current_user_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

class DummyCurrentUserPolicy < BasePolicy
class Scope < BasePolicy::BaseScope
def resolve
user
end
end
end

0 comments on commit 54cc64e

Please sign in to comment.