Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Add support for `params.expect` using `expected_parameters` and `expected_parameters_for`. [#855](https://github.com/varvet/pundit/pull/855)

## 2.5.2 (2025-09-24)

### Fixed
Expand Down
30 changes: 6 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -691,15 +691,12 @@ end

## Strong parameters

In Rails,
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:
In Rails, [mass-assignment protection is handled in the controller](https://guides.rubyonrails.org/action_controller_overview.html#strong-parameters). With Pundit you can control which attributes a user has access to update via your policies. You can set up an `expected_attributes_for_action(action_name)` method in your policy like this:

```ruby
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def permitted_attributes
def expected_attributes_for_action(_action_name)
if user.admin? || user.owner_of?(post)
[:title, :body, :tag_list]
else
Expand All @@ -726,19 +723,19 @@ class PostsController < ApplicationController
private

def post_params
params.require(:post).permit(policy(@post).permitted_attributes)
params.expect(policy(@post).expected_attributes)
end
end
```

However, this is a bit cumbersome, so Pundit provides a convenient helper method:
However, this is a bit cumbersome, so Pundit provides a convenient helper method with `#expected_attributes`:

```ruby
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def update
@post = Post.find(params[:id])
if @post.update(permitted_attributes(@post))
if @post.update(expected_attributes(@post))
redirect_to @post
else
render :edit
Expand All @@ -747,22 +744,7 @@ class PostsController < ApplicationController
end
```

If you want to permit different attributes based on the current action, you can define a `permitted_attributes_for_#{action}` method on your policy:

```ruby
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def permitted_attributes_for_create
[:title, :body]
end

def permitted_attributes_for_edit
[:body]
end
end
```

If you have defined an action-specific method on your policy for the current action, the `permitted_attributes` helper will call it instead of calling `permitted_attributes` on your controller.
Pundit still support the old `params.require.permit()` style of permitting attributes, although `params.expect()` is preferred.

If you need to fetch parameters based on namespaces different from the suggested one, override the below method, in your controller, and return an instance of `ActionController::Parameters`.

Expand Down
34 changes: 32 additions & 2 deletions lib/pundit/authorization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,36 @@ def pundit_policy_scope(scope)

# @!group Strong Parameters

# Retrieves a set of expected attributes from the policy.
#
# @example
# if @post.update(expected_attributes(@post))
# redirect_to @post
# else
# render :edit
# end
#
# @see https://github.com/varvet/pundit#strong-parameters
# @see https://guides.rubyonrails.org/action_controller_overview.html#expect
# @param record [Object] the object we're retrieving expected attributes for
# @param action [Symbol, String] the name of the action being performed on the record (e.g. `update`).
# If omitted then this defaults to the Rails controller action name.
# @param param_key [String] the key that the record would have in the params hash
# @return [Hash{String => Object}] the expected attributes
# @since v2.6.0
def expected_attributes(record, action: action_name, param_key: pundit_param_key(record))
policy = policy(record)
params.expect(param_key => policy.expected_attributes_for_action(action))
end

# @note This is provided as a hook for overrides.
# @param record [Object]
# @return [String] the key that the record would have in the params hash
# @since v2.6.0
def pundit_param_key(record)
PolicyFinder.new(record).param_key
end

# Retrieves a set of permitted attributes from the policy.
#
# Done by instantiating the policy class for the given record and calling
Expand All @@ -241,7 +271,7 @@ def pundit_policy_scope(scope)
#
# @see https://github.com/varvet/pundit#strong-parameters
# @param record [Object] the object we're retrieving permitted attributes for
# @param action [Symbol, String] the name of the action being performed on the record (e.g. `:update`).
# @param action [Symbol, String] the name of the action being performed on the record (e.g. `update`).
# If omitted then this defaults to the Rails controller action name.
# @return [Hash{String => Object}] the permitted attributes
# @since v1.0.0
Expand All @@ -261,7 +291,7 @@ def permitted_attributes(record, action = action_name)
# @return [ActionController::Parameters] the params
# @since v2.0.0
def pundit_params_for(record)
params.require(PolicyFinder.new(record).param_key)
params.require(pundit_param_key(record))
end

# @!endgroup
Expand Down
96 changes: 95 additions & 1 deletion spec/authorization_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

require "spec_helper"
require "action_controller/metal/strong_parameters"
require "action_controller"

describe Pundit::Authorization do
def to_params(*args, **kwargs, &block)
Expand Down Expand Up @@ -273,6 +273,100 @@ def to_params(*args, **kwargs, &block)
end
end

if ActionController::Parameters.method_defined?(:expect)
describe "#expected_attributes" do
it "checks policy for expected attributes" do
params = to_params(
post: {
title: "Hello",
votes: 5,
admin: true
}
)

action = "update"

expect(Controller.new(user, action, params).expected_attributes(post).to_h).to eq(
"title" => "Hello",
"votes" => 5
)
expect(Controller.new(double, action, params).expected_attributes(post).to_h).to eq("votes" => 5)
end

it "checks policy for expected attributes for record of a ActiveModel type" do
customer_post = Customer::Post.new(user)
params = to_params(
customer_post: {
title: "Hello",
votes: 5,
admin: true
}
)

action = "update"

expect(Controller.new(user, action, params).expected_attributes(customer_post).to_h).to eq(
"title" => "Hello",
"votes" => 5
)
expect(Controller.new(double, action, params).expected_attributes(customer_post).to_h).to eq(
"votes" => 5
)
end

it "goes through the policy cache" do
params = to_params(post: {title: "Hello"})
user = double
post = Post.new(user)
controller = Controller.new(user, "update", params)

expect do
expect(controller.expected_attributes(post)).to be_truthy
expect(controller.expected_attributes(post)).to be_truthy
end.to change { PostPolicy.instances }.by(1)
end
end

context "action-specific expected attributes" do
it "is checked if it is defined in the policy" do
params = to_params(
post: {
title: "Hello",
body: "blah",
votes: 5,
admin: true
}
)

action = "revise"
expect(Controller.new(user, action, params).expected_attributes(post).to_h).to eq("body" => "blah")
end

it "can be explicitly set" do
params = to_params(
post: {
title: "Hello",
body: "blah",
votes: 5,
admin: true
}
)

action = "update"
controller = Controller.new(user, action, params)
expect(controller.expected_attributes(post, action: :revise).to_h).to eq("body" => "blah")
end
end

it "can be retrieved with an explicit param key" do
params = to_params(admin_post: {title: "Hello"})

action = "update"
controller = Controller.new(user, action, params)
expect(controller.expected_attributes(post, param_key: "admin_post").to_h).to eq("title" => "Hello")
end
end

describe "#pundit_reset!" do
it "allows authorize to react to a user change" do
expect(controller.authorize(post)).to be_truthy
Expand Down
13 changes: 13 additions & 0 deletions spec/support/policies/post_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,17 @@ def permitted_attributes
def permitted_attributes_for_revise
[:body]
end

def expected_attributes_for_action(action_name)
case action_name.to_sym
when :revise
[:body]
else
if post.user == user
%i[title votes]
else
[:votes]
end
end
end
end