Rails announced strong_parameters
as a replacement for protected_attributes
nearly eight years ago. In 2020 many Rails apps have not completed the migration. Others have made the migration, but are worse off than before. Was strong_parameters
a bad idea? I don't think so. But like many things, it depends on how you use it.
Instances of ActionController:Parameters
leak outside of the controller
This is undoubtedly the most common mistake I've seen. The reason that strong_parameters
is superior to protected_attributes
is that it narrows the concern of mass-assignment protection to the controller. With protected_attributes
, the entire app had to consider which attributes could mass-assigned, and which could not. This led to anti-patterns like the use of without_protection
which allowed developers to skip mass-assignment protection under certain circumstances.
The benefit of strong_parameters
is lost when ActionController::Parameters
are passed outside of a controller and suddenly you have to be concerned about mass-assignment protection everywhere, again.
# FILE: app/lib/payment_processor.rb
class PaymentProcessor
def initialize(params)
@params = params
end
def process
# ... payment processing logic, etc
Payment.create(payment_params)
end
private
def payment_params
@params.require(:payment).permit(:some, :payment, :attrs)
end
end
# FILE: app/controllers/payments_controller.rb
class PaymentsController < ApplicationController
def create
payment = PaymentProcessor.new(params)
if payment.process
...
else
...
end
end
end
Strong Opinion: Parameters should be permitted at the controller level. If you pass some params on to another object (like a service class), first permit the params that are allowed for mass-assignment, then call to_h
on it to convert the object to a good ol' ActiveSupport::HashWithIndifferentAccess
.
# FILE: app/lib/payment_processor.rb
class PaymentProcessor
def initialize(payment_params)
@payment_params = payment_params
end
def process
# ... payment processing logic, etc
Payment.create(@payment_params)
end
end
# FILE: app/controllers/payments_controller.rb
class PaymentsController < ApplicationController
def create
payment = PaymentProcessor.new(payment_params.to_h)
if payment.process
...
else
...
end
end
private
def payment_params
params.require(:payment).permit(:some, :payment, :attrs)
end
end
Use of strong parameters where mass assignment is not being performed
The Rails community is pretty split on this one. Should you always permit parameters? Or only permit parameters when mass-assignment is being performed? Here's an example:
# FILE: app/controllers/users_controller.rb
class UsersController < ApplicationController
def special_update_action
user = User.find(user_params[:id])
user.special_attribute = user_params[:special_attribute]
if user.save
...
else
...
end
end
private
def user_params
params.require(:user).permit(
:id,
:special_attribute,
:some,
:user,
:attrs,
:including
)
end
end
Strong Opinion: Don't complicate your controllers by permitting params when mass assignment is not being performed.
# FILE: app/controllers/users_controller.rb
class UsersController < ApplicationController
def special_update_action
user = User.find(params[:id])
# Notice that special_attribute is pulled off of `params` directly
user.special_attribute = params[:special_attribute]
if user.save
...
else
...
end
end
end
Order of operations when mutating a params object
Remember, strong_parameters
is meant to sanitize externally-provided parameters. If you add a key to the params object, then call permit
, you'll end up having to permit the parameter you just added.
# FILE: app/controllers/credit_cards_controller.rb
class CreditCardsController < ApplicationController
def update
credit_card = CreditCard.find(params[:id])
params[:top_secret_token] = TopSecretToken.new(args)
if credit_card.update(credit_card_params)
...
else
...
end
end
private
def credit_card_params
# Notice the addition of top_secret_token is included in this
# scenario so that it can be mass-assigned above
params.require(:credit_card).permit(
:top_secret_token,
:some,
:credit_card,
:attrs
)
end
end
Strong Opinion: Permit your params before mutating the object. This allows you to permit only the externally-provided parameters, then modify the object as you see fit.
# FILE: app/controllers/credit_cards_controller.rb
class CreditCardsController < ApplicationController
def update
credit_card = CreditCard.find(params[:id])
assignable_params = credit_card_params
assignable_params[:top_secret_token] = TopSecretToken.new(args)
if credit_card.update(assignable_params)
...
else
...
end
end
private
def credit_card_params
# Notice that top_secret_token does *not* need to be included
# here because it is assigned to the (permitted) return value
# of credit_card_params
params.require(:credit_card).permit(:some, :credit_card, :attrs)
end
end
Defining permitted attributes multiple (sometimes many) times
With protected_attributes
, the model provided a central place to specify which attributes were permitted for mass-assignment. With strong_parameters
, permitted parameters are often defined multiple times for the same resource in different controllers. This is not DRY and it is not maintainable. Add in consideration for accepts_nested_attributes_for
and you're maintaining countless lists of the same parameters.
Note: In the previous examples parameters were permitted directly in the controller as seen below. This was done for simplicity's sake, and also on the basis that no other controllers duplicated the same set of permitted parameters.
# FILE: app/controllers/departments_controller.rb
class DepartmentsController < ApplicationController
def create
department = Department.new(department_params)
if department.save
...
else
...
end
end
private
def department_params
params.require(:department).permit(
:some, :department, :attrs,
employees_attributes: [
:some, :employee, :attrs
]
)
end
end
# FILE: app/controllers/special/departments_controller.rb
module Special
class DepartmentsController < ApplicationController
def create
department = Department.new(department_params)
if department.save
...
else
...
end
end
private
# Copy pasta
def department_params
params.require(:department).permit(
:some, :department, :attrs,
employees_attributes: [
:some, :employee, :attrs
]
)
end
end
end
Strong Opinion: Duplicated parameters should be permitted in a module that can be included wherever it is needed. Furthermore, nested attributes should not be redefined multiple times. Here's an example:
# FILE: app/controllers/concerns/strong_parameters/employee.rb
module Concerns
module StrongParameters
module Employee
def employee_params
params.require(:employee).permit(*self.permitted_attrs)
end
def self.permitted_attrs
%i(some employee attrs)
end
end
end
end
# FILE: app/controllers/concerns/strong_parameters/department.rb
module Concerns
module StrongParameters
module Department
def department_params
params.require(:department).permit(*self.permitted_attrs)
end
def self.permitted_attrs
[
:some, :department, :attrs,
{ employees_attributes: [
:id, :_destroy, *Concerns::StrongParameters::Employee.permitted_attrs] }
]
end
end
end
end
# FILE: app/controllers/departments_controller.rb
class DepartmentsController < ApplicationController
include Concerns::StrongParameters::Department
def create
department = Department.new(department_params)
if department.save
...
else
...
end
end
end
# FILE: app/controllers/special/departments_controller.rb
module Special
class DepartmentsController < ApplicationController
include Concerns::StrongParameters::Department
def create
department = Department.new(department_params)
if department.save
...
else
...
end
end
end
end
# FILE: app/controllers/employees_controller.rb
class EmployeesController < ApplicationController
include Concerns::StrongParameters::Employee
def create
employee = employee.new(employee_params)
if employee.save
...
else
...
end
end
end
Summary
We follow these practices at Hint, and it's helped us (and our clients) a lot. Does your organization need help wrangling strong parameters? We can help. Ping me on twitter: @benjaminwood .
Originally published on the hint.io blog here: https://hint.io/blog/strong-parameters-strong-opinions
Top comments (1)
Self reflection :
So I am unlikely to bring myself to completely read this, but I can this is a pretty unique discussion topic.
I'm intrigued, what is this problem? How is it being addressed?
But you say words I don't know, talk specifically about features I haven't heard of.
See I don't program in Ruby. But I don't program in many languages, but still can pick out what their programming challenge is. I wonder if I'll come back at a later time when I'm prepared to put more mental energy in.
Thank you.