Experience Upgrading to Strong Params

In our workplace app, most models are used by different code paths (e.g. a customer and internal user creating the same resource). Using attr_accessible, all we could do is to permit a large set of attributes except the ones explicitly assigned by the system (a lousy example, created_at). Then we go about slicing the params in the controller as a first level defense for mass assignment from user input.

Hence, I've been eagerly waiting to upgrade our app to use strong params on Rails 4. Here's my experience upgrading our largest codebase.

Protected Attributes?

In Rails 4, attr_accessible is extracted to a gem called protected_attributes. We wanted the app to initialize successfully whilst changing to strong params, so we added it. (gem 'protected_attributes' to Gemfile)

We turned off whitelisting so Rails doesn't moan at unprotected attributes (config.active_record.whitelist_attributes = false to application.rb)

With this setup, protected attributes plays nice with Rails 4, the next thing was basically to trawl the codebase for methods to refactor. Using tests you don't say? Well, bundling Rails 4 already caused us a red sea of spec failures + some specs came up with mysterious failures, unfortunately, find all comes in really handy.

Three uses of attr_accessible

Finding all for attr_accessible only turned up three uses. Woo hoo! Thank you guys, erm, hopefully we sliced everywhere. This should be easy. In upgrading, we're trying our best to practice 1-to-1 refactoring. I.e, we do not want to do any refactoring that might change business logic. That comes later. The refactoring started out as

  • Find all controllers that builds the said model.
  • Put the permitted params in the model into the controller instead using strong params.

That worked well, BUT...

Protip 1: Don't forget attr_protected. It's part of model security too.

attr_protected works the opposite way. Instead of whitelisting attribute for mass assignment, it says this attribute cannot be mass assigned.

Now that's not easy. Currently, strong_params doesn't seem to have a blacklisting mode. If you said the logic

Allow everything except for the token attribute.

This might translate to something like

# Slice off the protected one

# Alternatively with less side effects
params.require(:my_model).permit(my_model.columns.names - [:token, :created_at])

You can see how this might turn ugly in the long run. White listing is a better practice than blacklisting, but there must be quite a few use cases where blacklisting is useful. Let's watch strong_params for this. Currently, we stuck to direct assignment of the token attribute and avoided both solutions above.

Protip 2: It works in models? HUH?

We know strong_params is about shifting security to the controller. But how does it work? Well, of course, through the model! =). Strong Params guards against passing in unpermitted parameters. For example,

  # It blocks in create
  def create
    person = Person.new(person_params) # Works
    person = Person.new(params[:person]) # Forbidden Attributes Error

  # It blocks in new too.
  def new
    person = Person.new(person_params) # Works
    person = Person.new(params[:person]) # Forbidden Attributes Error

  def person_params
    params.require(:person).permit(:name, :date_of_birth)

Using methods like build, create and new raises ActiveModel::ForbiddenAttributesError. It is NOT dependent on the controller action. This means that it's still linked to models in unexpected ways. The params hash is a special hash now belonging to Action:Controller::Parameters. For a simple test,

  params = ActionController::Parameters.new(person: { name: "Damon"})
  params[:person].permitted? # => false
  Person.new(params[:person]) # => Forbidden Attributes Error

  # Let's clear it
  permitted = params.permit(person: [:name])
  Person.new(params[:person]) # Still fails
  Person.new(permitted) # Passes. You have to whitelist everytime on params

Side Effects

In our app, we stick pretty close to the thin controller, fat model idiom. This introduces a side effect like this.

  def some_controller_action
    # Person builder is not an AR / ActiveModel object
    person_builder = PersonBuilder.new(params)
    render "person", person: person_builder.person

  def somewhere_in_person_builder
    Person.new(params) # Hey hey... Forbidden Attributes Error

Protip 3: Empty roots are not good enough

  # params = { person: { name: "Damon" }, role: "" }

  def person_params

  def role_params
    params.require(:role).permit! # Require is going to throw an error.

Protip 4: Make your guys remember to use the Root

This is a person gribe, after refactoring over 40 controllers, I would say the Rails convention of having a root for resources to be pretty solid. This allows you to use params.require(:some_root) and it cannot be clearer what these attributes belong to.

So if you see anyone in your team discard the root resource bacause XYZ framework doesn't do it, find a way to do it =)