Skip to content

Commit d53c841

Browse files
authored
Merge pull request #855 from Linuus/expected-attributes
Add support for params.expect
2 parents 7aa30eb + c6b7b25 commit d53c841

File tree

5 files changed

+148
-27
lines changed

5 files changed

+148
-27
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Add support for `params.expect` using `expected_parameters` and `expected_parameters_for`. [#855](https://github.com/varvet/pundit/pull/855)
6+
57
## 2.5.2 (2025-09-24)
68

79
### Fixed

README.md

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -691,15 +691,12 @@ end
691691

692692
## Strong parameters
693693

694-
In Rails,
695-
mass-assignment protection is handled in the controller. With Pundit you can
696-
control which attributes a user has access to update via your policies. You can
697-
set up a `permitted_attributes` method in your policy like this:
694+
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:
698695

699696
```ruby
700697
# app/policies/post_policy.rb
701698
class PostPolicy < ApplicationPolicy
702-
def permitted_attributes
699+
def expected_attributes_for_action(_action_name)
703700
if user.admin? || user.owner_of?(post)
704701
[:title, :body, :tag_list]
705702
else
@@ -726,19 +723,19 @@ class PostsController < ApplicationController
726723
private
727724

728725
def post_params
729-
params.require(:post).permit(policy(@post).permitted_attributes)
726+
params.expect(policy(@post).expected_attributes)
730727
end
731728
end
732729
```
733730

734-
However, this is a bit cumbersome, so Pundit provides a convenient helper method:
731+
However, this is a bit cumbersome, so Pundit provides a convenient helper method with `#expected_attributes`:
735732

736733
```ruby
737734
# app/controllers/posts_controller.rb
738735
class PostsController < ApplicationController
739736
def update
740737
@post = Post.find(params[:id])
741-
if @post.update(permitted_attributes(@post))
738+
if @post.update(expected_attributes(@post))
742739
redirect_to @post
743740
else
744741
render :edit
@@ -747,22 +744,7 @@ class PostsController < ApplicationController
747744
end
748745
```
749746

750-
If you want to permit different attributes based on the current action, you can define a `permitted_attributes_for_#{action}` method on your policy:
751-
752-
```ruby
753-
# app/policies/post_policy.rb
754-
class PostPolicy < ApplicationPolicy
755-
def permitted_attributes_for_create
756-
[:title, :body]
757-
end
758-
759-
def permitted_attributes_for_edit
760-
[:body]
761-
end
762-
end
763-
```
764-
765-
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.
747+
Pundit still support the old `params.require.permit()` style of permitting attributes, although `params.expect()` is preferred.
766748

767749
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`.
768750

lib/pundit/authorization.rb

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,36 @@ def pundit_policy_scope(scope)
231231

232232
# @!group Strong Parameters
233233

234+
# Retrieves a set of expected attributes from the policy.
235+
#
236+
# @example
237+
# if @post.update(expected_attributes(@post))
238+
# redirect_to @post
239+
# else
240+
# render :edit
241+
# end
242+
#
243+
# @see https://github.com/varvet/pundit#strong-parameters
244+
# @see https://guides.rubyonrails.org/action_controller_overview.html#expect
245+
# @param record [Object] the object we're retrieving expected attributes for
246+
# @param action [Symbol, String] the name of the action being performed on the record (e.g. `update`).
247+
# If omitted then this defaults to the Rails controller action name.
248+
# @param param_key [String] the key that the record would have in the params hash
249+
# @return [Hash{String => Object}] the expected attributes
250+
# @since v2.6.0
251+
def expected_attributes(record, action: action_name, param_key: pundit_param_key(record))
252+
policy = policy(record)
253+
params.expect(param_key => policy.expected_attributes_for_action(action))
254+
end
255+
256+
# @note This is provided as a hook for overrides.
257+
# @param record [Object]
258+
# @return [String] the key that the record would have in the params hash
259+
# @since v2.6.0
260+
def pundit_param_key(record)
261+
PolicyFinder.new(record).param_key
262+
end
263+
234264
# Retrieves a set of permitted attributes from the policy.
235265
#
236266
# Done by instantiating the policy class for the given record and calling
@@ -241,7 +271,7 @@ def pundit_policy_scope(scope)
241271
#
242272
# @see https://github.com/varvet/pundit#strong-parameters
243273
# @param record [Object] the object we're retrieving permitted attributes for
244-
# @param action [Symbol, String] the name of the action being performed on the record (e.g. `:update`).
274+
# @param action [Symbol, String] the name of the action being performed on the record (e.g. `update`).
245275
# If omitted then this defaults to the Rails controller action name.
246276
# @return [Hash{String => Object}] the permitted attributes
247277
# @since v1.0.0
@@ -261,7 +291,7 @@ def permitted_attributes(record, action = action_name)
261291
# @return [ActionController::Parameters] the params
262292
# @since v2.0.0
263293
def pundit_params_for(record)
264-
params.require(PolicyFinder.new(record).param_key)
294+
params.require(pundit_param_key(record))
265295
end
266296

267297
# @!endgroup

spec/authorization_spec.rb

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
require "spec_helper"
4-
require "action_controller/metal/strong_parameters"
4+
require "action_controller"
55

66
describe Pundit::Authorization do
77
def to_params(*args, **kwargs, &block)
@@ -273,6 +273,100 @@ def to_params(*args, **kwargs, &block)
273273
end
274274
end
275275

276+
if ActionController::Parameters.method_defined?(:expect)
277+
describe "#expected_attributes" do
278+
it "checks policy for expected attributes" do
279+
params = to_params(
280+
post: {
281+
title: "Hello",
282+
votes: 5,
283+
admin: true
284+
}
285+
)
286+
287+
action = "update"
288+
289+
expect(Controller.new(user, action, params).expected_attributes(post).to_h).to eq(
290+
"title" => "Hello",
291+
"votes" => 5
292+
)
293+
expect(Controller.new(double, action, params).expected_attributes(post).to_h).to eq("votes" => 5)
294+
end
295+
296+
it "checks policy for expected attributes for record of a ActiveModel type" do
297+
customer_post = Customer::Post.new(user)
298+
params = to_params(
299+
customer_post: {
300+
title: "Hello",
301+
votes: 5,
302+
admin: true
303+
}
304+
)
305+
306+
action = "update"
307+
308+
expect(Controller.new(user, action, params).expected_attributes(customer_post).to_h).to eq(
309+
"title" => "Hello",
310+
"votes" => 5
311+
)
312+
expect(Controller.new(double, action, params).expected_attributes(customer_post).to_h).to eq(
313+
"votes" => 5
314+
)
315+
end
316+
317+
it "goes through the policy cache" do
318+
params = to_params(post: {title: "Hello"})
319+
user = double
320+
post = Post.new(user)
321+
controller = Controller.new(user, "update", params)
322+
323+
expect do
324+
expect(controller.expected_attributes(post)).to be_truthy
325+
expect(controller.expected_attributes(post)).to be_truthy
326+
end.to change { PostPolicy.instances }.by(1)
327+
end
328+
end
329+
330+
context "action-specific expected attributes" do
331+
it "is checked if it is defined in the policy" do
332+
params = to_params(
333+
post: {
334+
title: "Hello",
335+
body: "blah",
336+
votes: 5,
337+
admin: true
338+
}
339+
)
340+
341+
action = "revise"
342+
expect(Controller.new(user, action, params).expected_attributes(post).to_h).to eq("body" => "blah")
343+
end
344+
345+
it "can be explicitly set" do
346+
params = to_params(
347+
post: {
348+
title: "Hello",
349+
body: "blah",
350+
votes: 5,
351+
admin: true
352+
}
353+
)
354+
355+
action = "update"
356+
controller = Controller.new(user, action, params)
357+
expect(controller.expected_attributes(post, action: :revise).to_h).to eq("body" => "blah")
358+
end
359+
end
360+
361+
it "can be retrieved with an explicit param key" do
362+
params = to_params(admin_post: {title: "Hello"})
363+
364+
action = "update"
365+
controller = Controller.new(user, action, params)
366+
expect(controller.expected_attributes(post, param_key: "admin_post").to_h).to eq("title" => "Hello")
367+
end
368+
end
369+
276370
describe "#pundit_reset!" do
277371
it "allows authorize to react to a user change" do
278372
expect(controller.authorize(post)).to be_truthy

spec/support/policies/post_policy.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,17 @@ def permitted_attributes
3333
def permitted_attributes_for_revise
3434
[:body]
3535
end
36+
37+
def expected_attributes_for_action(action_name)
38+
case action_name.to_sym
39+
when :revise
40+
[:body]
41+
else
42+
if post.user == user
43+
%i[title votes]
44+
else
45+
[:votes]
46+
end
47+
end
48+
end
3649
end

0 commit comments

Comments
 (0)