Strong Opinions About Strong Parameters

Benjamin Wood - April 10, 2020

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.

Strong Opinions

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.

Benjamin Wood

Ben is a family man and partner at Hint. When not shipping software with the team at Hint, you'll likely find him spending time with his wife and two children.

  
  

Ready to Get Started?

LET'S CONNECT